Skip to main content

SQL injection and safe queries

The most common and most damaging database vulnerability has one cause: building SQL by gluing user input into a query string. When input becomes part of the SQL text, an attacker can rewrite your query. The fix is simple and absolute - and you should adopt it as a reflex.

The attack

Say a login looks up a user by name, and the app builds the query by concatenation:

// DANGEROUS - never do this
const sql = "SELECT * FROM users WHERE name = '" + input + "'";

For a normal name it works. But an attacker types ' OR '1'='1 as the name. The string becomes ... WHERE name = '' OR '1'='1' - always true - and every row comes back. Run it:

Loading SQL sandbox…

Every user and token leaked. Worse inputs can read other tables, or with stacked statements like '; DROP TABLE users; -- destroy data. The query did exactly what the text said - the problem is that the attacker got to write part of the text.

The fix: parameterized queries

Never assemble SQL from input. Use parameterized queries (also called prepared statements): write placeholders, and pass the values separately. The driver sends the SQL and the data on different channels, so input is only ever treated as a value, never as SQL to parse.

// safe - the driver binds `input` as a value
db.query('SELECT * FROM users WHERE name = $1', [input]);
# safe - placeholders, values passed separately
cur.execute("SELECT * FROM users WHERE name = %s", (input,))

Now ' OR '1'='1 is searched for literally as a name - it matches nothing, exactly as intended. ORMs and query builders parameterize by default, which is a big part of why they improve safety. There is no performance reason to concatenate; parameterized queries are also faster to reuse.

Defense in depth

Parameterizing is the main fix. Layer more behind it:

  • Least privilege. If the app's role can only SELECT what it needs, an injection that slips through can do far less. (See DCL.)
  • Validate and allowlist. You cannot parameterize a table or column name - placeholders are for values only. When SQL must include a dynamic identifier (a sort column, say), check it against an allowlist of known-good names; never interpolate raw input.
  • Avoid dynamic SQL where you can; keep generated SQL minimal and reviewed.

The rule in 2026 is unchanged because the fix has never changed: parameterize every query that touches user input. One extra note - SQL generated by an LLM or copied from the web deserves the same scrutiny; review it for concatenated input before it ships.

Quick quiz

SQL injection

4 questions

1What is the root cause of SQL injection?

2How do parameterized queries prevent injection?

3You need to let users sort by a column they pick. Why can't you just parameterize the column name?

4Which extra layer limits the damage if an injection does get through?

Next up

ORMs and migrations - how application code talks to the database, and how the schema evolves safely over time.