SQL injection and parameterised queries

Backend security Junior Python 3.13+ PostgreSQL 17+ Reviewed By Best Latest Apps

Predict the output

What do the two lines print?

Python 3.13
import psycopg

with psycopg.connect(autocommit=True) as conn:
    conn.execute("CREATE TEMP TABLE users (name text, admin bool)")
    conn.execute(
        "INSERT INTO users VALUES ('ana', false), ('root', true)"
    )
    evil = "x' OR '1'='1"
    unsafe = f"SELECT count(*) FROM users WHERE name = '{evil}'"
    print(conn.execute(unsafe).fetchone())  # injectable
    safe = "SELECT count(*) FROM users WHERE name = %s"
    print(conn.execute(safe, (evil,)).fetchone())

Options
Hint 1 of 1

What does the WHERE clause become after formatting?

Choose an answer, then open Show answer below to check it.

Show answer

Answer: A (2,) then (0,)

Correct. The f-string turns the input into OR '1'='1', matching every row; the parameter is compared as a plain string.

The f-string pastes the input into the SQL text, so the quote ends the string and OR '1'='1' becomes part of the query. A bound parameter is sent separately and compared as data.

Why not the others?
  • B (0,) then (0,). The f-string version is rewritten by the input.
  • C (2,) then (2,). Parameters are never parsed as SQL.
  • D An error, then (0,). The injected SQL is valid.

30-second answer

Injection happens when user input is pasted into SQL text, so a value like x' OR '1'='1 changes the query's meaning. Parameterised queries send the SQL and the values separately; the database never parses the value as SQL, so the attack becomes a harmless string. Use parameters for every value, an allow-list for identifiers you can't parameterise (column names, sort direction), and a database role with only the rights the app needs.

Practise saying it · 30 s. No recording, just a timer

Explain SQL injection.

Say your answer out loud, then check which of these points you covered:

  • Input becomes SQL
  • Parameters separate data
  • Allow-list identifiers
  • Least privilege

Deep dive

psycopg's sql.Identifier safely quotes dynamic table or column names.

ORMs are safe until someone uses raw string formatting in them.

Escaping by hand is error-prone; don't.

Least-privilege roles limit the damage if an injection slips through.

Unsafe f-string vs a parameter Ran on Python 3.13.5 · mypy --strict
Python 3.13
import psycopg

with psycopg.connect(autocommit=True) as conn:
    conn.execute("CREATE TEMP TABLE users (name text, admin bool)")
    conn.execute(
        "INSERT INTO users VALUES ('ana', false), ('root', true)"
    )
    evil = "x' OR '1'='1"
    unsafe = f"SELECT count(*) FROM users WHERE name = '{evil}'"
    print(conn.execute(unsafe).fetchone())  # injectable
    safe = "SELECT count(*) FROM users WHERE name = %s"
    print(conn.execute(safe, (evil,)).fetchone())

Common mistakes

  • f-strings or + in SQL.
  • Sanitising input with a blacklist of characters.
  • Parameterising values but formatting ORDER BY from the request.

Senior take & follow-ups

Show the broken query, the parameterised fix, and allow-lists for identifiers.

Follow-up questions

Think of your answer first, then open the hint.

  1. How do you sort by a user-chosen column?

    Show hint

    Map the request to an allow-listed column name, then use sql.Identifier.

  2. Do stored procedures prevent injection?

    Show hint

    Only if they don't build dynamic SQL from input.

Suggest a fix if something here is wrong or out of date.