SQL Injection in a Parameterized Query, Is It Still Possible
Parameterized queries eliminate the classic SQL injection vector, where user input is concatenated directly into a query string — but they don't make injection impossible in every case, since a few specific patterns can still bypass their protection if not handled correctly.
The Confusion
Parameterized queries (or prepared statements) are correctly taught as the standard, reliable defense against SQL injection, which leads some developers to assume any query using them is automatically completely safe, regardless of how the rest of the query is constructed. This isn't quite accurate — parameterization protects the specific values passed as parameters, but doesn't protect other parts of a query that might still be built through string concatenation.
What Parameterized Queries Actually Protect
A parameterized query separates the SQL structure from the data values, sending them to the database separately rather than combining them into a single string that the database has to parse as one unit:
// Safe: value is a parameter, never interpreted as SQL syntax
const query = 'SELECT * FROM users WHERE email = ?';
db.execute(query, [userInput]);
Because the database receives the value separately from the query structure, it's treated purely as data — even if userInput contains something like ' OR '1'='1, it's matched literally against the email column, not interpreted as SQL syntax at all. This is why parameterization is such an effective defense: it doesn't rely on escaping or filtering potentially dangerous characters, it structurally prevents the value from ever being interpreted as code.
Where Injection Can Still Happen
The protection only applies to values passed as actual parameters — anything else built through string concatenation remains vulnerable, even in code that also uses parameterized queries elsewhere:
- Dynamic table or column names — parameters can only represent values, not identifiers like table or column names, so code that needs to dynamically select a table name based on user input often falls back to string concatenation for that specific part, reintroducing the vulnerability
- Dynamic ORDER BY or sort direction — similarly, sorting logic driven by user-selectable column names or sort direction frequently ends up concatenated rather than parameterized, since these also aren't simple data values
- Raw query building through string formatting elsewhere in the codebase, even if a different part of the same application correctly uses parameterization — a single unsafe query anywhere in the codebase is enough to be exploitable, regardless of how many other queries are done correctly
- ORM "raw query" escape hatches that bypass the ORM's normal parameterized query building, often used when a developer needs a complex query the ORM's standard API doesn't easily support, and reaches for string concatenation as a shortcut
The Fix
For dynamic table or column names, use an allowlist that validates the input against a fixed set of known-safe values, rather than passing user input directly into the query string:
const allowedColumns = ['name', 'created_at', 'email'];
if (!allowedColumns.includes(requestedColumn)) {
throw new Error('Invalid sort column');
}
const query = `SELECT * FROM users ORDER BY ${requestedColumn}`;
This is safe specifically because the value is checked against a fixed, trusted list before ever being used in string construction — the user's input can only select among values you've already deemed safe, never inject arbitrary SQL syntax.
Audit any use of an ORM's raw query or raw expression features specifically, since these are common blind spots where a team correctly uses the ORM's safe query builder everywhere else, but drops into unsafe string concatenation in the few places the builder felt too limiting:
// Still vulnerable, even though it's inside an ORM that uses
// parameterized queries everywhere else by default
db.raw(`SELECT * FROM users WHERE email = '${userInput}'`);
// Correct: even raw queries usually support parameter placeholders
db.raw('SELECT * FROM users WHERE email = ?', [userInput]);
Run static analysis tools specifically designed to detect SQL injection patterns across a codebase, rather than relying solely on manual code review to catch every instance — these tools are generally good at flagging string concatenation feeding into query execution functions, even in large codebases where manually reviewing every query isn't practical.
For genuinely necessary dynamic identifiers that can't be reduced to a simple allowlist (rare, but possible in some multi-tenant or highly dynamic schema scenarios), use your database driver's specific identifier-quoting function if one is provided, which handles proper escaping for identifiers specifically — a different mechanism from value parameterization, but serving an analogous safety purpose for the identifier case.
Still Confused?
A useful mental model: parameterized queries protect values, not structure. Anything that represents part of the query's actual SQL syntax — table names, column names, sort direction, the operators themselves — falls outside what parameterization can protect, and needs a different defense (typically allowlisting against known-safe options) specifically for that part of the query.