Varol Cagdas Tok

Personal notes and articles.

SQL Injection Defense: Parameterized Queries

Parameterized queries, also called prepared statements, prevent SQL injection by separating query structure from query data at the protocol level. The database receives and parses the query template before it receives the user-supplied values. Once the structure is parsed, the parameter values cannot alter it, regardless of what characters they contain.


What Happens at the Protocol Level

Modern database client protocols support a prepare/execute cycle that is distinct from sending a complete SQL string. In PostgreSQL's extended query protocol, for example, the client sends a Parse message containing the query with parameter placeholders (\(1`, `\)2, etc.) and no data. The server parses this template and stores the parsed representation. The client then sends a Bind message with the actual parameter values as typed binary or text data. The server substitutes the values into the parsed query plan without re-parsing. The Execute message runs it.

The parser never processes the parameter values as SQL text. The string '; DROP TABLE users;-- arrives as a string value in the Bind message. The database treats it as a literal string and passes it to the query plan as data. It is never tokenized as SQL syntax.


Emulated vs. True Prepared Statements

Not all database drivers implement the full prepare/execute protocol. Some drivers emulate prepared statements by escaping parameter values and inserting them into the query string client-side before sending a complete SQL string to the database. This provides protection only as good as the escaping implementation, which can have gaps depending on character encoding, database configuration, and edge cases in quote handling.

PHP's PDO, for example, defaults to emulated prepared statements. Real prepared statements require setting PDO::ATTR_EMULATE_PREPARES to false. MySQL's native prepared statement protocol is used when emulation is disabled. The difference matters: with emulation, a multibyte encoding edge case could allow a quote character to appear unescaped; with native prepared statements, parameter values are never in the SQL string at all.


What Parameterization Does Not Cover

Parameterization binds values. It cannot parameterize SQL identifiers: table names, column names, ORDER BY references, or other structural elements. A query like:

SELECT * FROM ? WHERE id = ?

is not valid SQL; the table name position is not a bindable parameter in any standard SQL database. Dynamic identifiers must be handled with allowlists: the application maps user input to one of a fixed set of known-safe identifier strings, then interpolates the allowlisted value. This is not parameterization, but it achieves the same safety property for the identifier position by ensuring user input never reaches the SQL string.


Stored Procedures

Stored procedures prevent injection only when they use parameterized SQL internally. A stored procedure that builds SQL strings through concatenation and executes them with EXEC or sp_executesql has the same vulnerability as application-level concatenation. The procedure boundary does not sanitize input; it only moves the concatenation into the database engine.