WierdX — Programming Reference All tutorials →
Developer reference · Practical tutorials · CS fundamentals
Security

SQL Injection and Parameterized Queries: How Untrusted Input Breaks Databases

SQL injection has been on every "most common vulnerability" list for over two decades, not because it's subtle, but because string-concatenated queries keep getting written anyway. The fix has been well understood the entire time.

Published July 6, 2026

A SQL injection vulnerability happens when user input gets pasted directly into a query string instead of being kept separate from it. The database has no way to tell the difference between "data the query is operating on" and "syntax the query is made of" once they're concatenated into the same string — it just parses whatever text it receives. That's the entire vulnerability in one sentence, and everything else is a consequence of it.

What it looks like when it breaks

Take a login check built with string concatenation:

query = "SELECT * FROM users WHERE username = '" + username +
        "' AND password = '" + password + "'";

With normal input, say a username of alice, this produces a reasonable query. But nothing stops the input from containing SQL syntax of its own. Submit a username of ' OR '1'='1 and the concatenated string becomes:

SELECT * FROM users WHERE username = '' OR '1'='1' AND password = ''

'1'='1' is always true, so this returns every row in the table, and depending on how the application handles a multi-row result from a login check, that can mean logging in as the first user in the table without knowing any password at all. A slightly more aggressive payload can append a second statement entirely — '; DROP TABLE users; -- — if the driver and database allow stacked queries, turning a login form into a way to delete data. The attacker isn't exploiting a bug in the database engine; the engine is doing exactly what SQL says to do. The bug is that attacker-controlled text was allowed to become part of the query's syntax in the first place.

The fix: keep data and syntax apart

A parameterized query (also called a prepared statement) sends the query structure and the data as two separate things over the wire, instead of building the query as one string:

query = "SELECT * FROM users WHERE username = ? AND password_hash = ?";
db.execute(query, [username, hashedPassword]);

The database parses and compiles the query with placeholders first, establishing its exact structure before it ever sees the user's values. The parameters get bound afterward, purely as data, with no opportunity to be interpreted as SQL syntax no matter what characters they contain. A malicious username like ' OR '1'='1 is compared, literally, against the username column — it just fails to match, because no user has that exact string as a username. This isn't input sanitization or escaping quotes; it's a structurally different mechanism where the query the database executes is fixed before user data ever enters the picture, which is why it closes the entire class of vulnerability rather than blocking specific attack patterns.

Why escaping isn't the same fix

Escaping quotes in user input (turning ' into \' or '') was the older mitigation, and it's fragile in ways parameterized queries aren't. Escaping rules differ by database engine and by character encoding, and historically, subtle encoding mismatches (multi-byte character sets that produced a valid escape-breaking sequence) let attackers bypass naive escaping entirely. Escaping also has to be applied correctly at every single point user input reaches a query, with no exceptions, forever, across every developer who ever touches the codebase. Parameterization doesn't rely on getting that discipline right every time, because the separation between code and data is enforced by the database driver itself rather than by a function call someone might forget.

Where ORMs help and where they don't

Most modern ORMs use parameterized queries under the hood for anything expressed through their standard query builder methods, which is a large part of why injection has become rarer in codebases that lean on an ORM for typical CRUD operations. But every ORM also offers an escape hatch for raw SQL — a method like .raw() or .execute(rawString) — for queries too complex to express through the builder, and that escape hatch is exactly as vulnerable as hand-written string concatenation if user input gets interpolated into it:

// Still vulnerable, ORM or not
db.raw(`SELECT * FROM orders WHERE status = '${status}'`);

// Safe: parameters passed separately, even through raw()
db.raw('SELECT * FROM orders WHERE status = ?', [status]);

The pattern to watch for in code review isn't "does this project use an ORM," it's "does any string containing user input get built with concatenation or template literals and then handed to the database as a query," regardless of whether an ORM sits on top of that call. Dynamic table or column names are the one case parameterization genuinely can't cover, since placeholders bind values, not identifiers — those need a strict allowlist check against known-valid names before being inserted into the query string, never a value taken directly from a request. The OWASP SQL Injection Prevention Cheat Sheet is the standard reference for the edge cases (stored procedures, dynamic identifiers, second-order injection) that a short tutorial like this one doesn't have room to cover.