toolhq.io

All posts
6 min readby Jameel Haider

SQL injection and parameterized queries: the root cause and the fix

SQL injection happens when data supplied by a user is treated as part of a SQL command instead of as a value. The attacker does not break into anything. They simply type input that, once glued into your query string, changes what the query means. The root cause is always the same: the query text and the data were combined as strings before the database parsed them.

How the attack works

Consider a login check built by concatenation:

"SELECT * FROM users WHERE name = '" + input + "'"

If input is alice, the query is harmless. If input is ' OR '1'='1, the query becomes:

SELECT * FROM users WHERE name = '' OR '1'='1'

'1'='1' is always true, so the WHERE matches every row. The attacker's quote closed your string early, and everything after it became SQL. A more destructive payload like '; DROP TABLE users; -- closes the string, adds a second statement, and comments out the rest. The database cannot tell the difference, because by the time it sees the query, the malicious text is indistinguishable from the SQL you wrote. There is no boundary left between code and data.

Why escaping by hand is not the fix

The tempting response is to escape the input: double up the quotes, strip semicolons, filter keywords. This fails in practice for several reasons:

  • Different databases have different escaping rules and string syntaxes, so a filter that is correct for one is wrong for another.
  • Numeric and identifier contexts have no quotes to escape, so a numeric parameter like id = 5 OR 1=1 injects without ever using a quote.
  • Character set and encoding tricks can smuggle a quote past a naive filter.
  • Blocklists of dangerous words are always incomplete, and they break legitimate input such as a user named O'Brien.

Hand escaping treats the symptom. The disease is that code and data were mixed at all.

Parameterized queries: the real fix

A parameterized query, also called a prepared statement, sends the query structure and the data to the database separately. You write the SQL with placeholders, then hand the values over as a distinct list. The database parses and plans the query first, with the placeholders as holes, and only then binds the values into those holes. A bound value can never change the parsed structure, because parsing already finished. It is pure data by construction.

-- the query, parsed once, with a placeholder
SELECT * FROM users WHERE name = ?
# the value, bound separately, never parsed as SQL
cursor.execute("SELECT * FROM users WHERE name = ?", (user_input,))

Now ' OR '1'='1 arriving as the value is searched for literally. The database looks for a user whose name is the ten character string ' OR '1'='1, finds none, and returns nothing. The payload has no power because it never reaches the parser as code.

Placeholder syntax varies by driver:

Driver / databasePlaceholder style
SQLite, ODBC, JDBC? positional
PostgreSQL (psycopg, pg)$1, $2 numbered
Python DB-API (named):name
MySQL connector%s

The one thing parameters cannot do

Bind parameters work for values: things in a WHERE, VALUES, or SET. They cannot parameterize identifiers such as a table name, a column name, or a sort direction, because those are part of the query structure, not data. If you must build a query with a dynamic column or table name, you cannot bind it. The safe approach there is a strict allowlist: compare the input against a fixed set of known good identifiers and use the matched constant, never the raw input.

The rule

Never build SQL by concatenating or interpolating untrusted input. Use parameterized queries for every value, every time, including internal tools, because today's internal field is tomorrow's public endpoint. An ORM or query builder that parameterizes under the hood is fine; what matters is that the value never reaches the parser as text.

When you are reviewing a long query for structure and want it readable first, the SQL Formatter tidies it locally in your browser without sending it anywhere.