How to Fix SQL Injection With Parameterized Queries

An f-string SQL query treats user input as SQL code - a classic ' OR 1=1 -- payload returns every row in your database. The fix is parameterized queries: the database driver escapes the value before it reaches the parser, so the payload becomes a harmless literal string.

Security Engineerpythonflaskpostgresql

What makes f-string SQL dangerous

A search endpoint that builds queries with string formatting is trivially exploitable:

# Vulnerable - never do this
@app.get("/search")
def search():
    q = request.args.get("q", "")
    query = f"SELECT id, username, email FROM users WHERE username LIKE '%{q}%'"
    cur.execute(query)
    return jsonify(cur.fetchall())

An attacker sends q=' OR 1=1-- and the query becomes:

SELECT id, username, email FROM users WHERE username LIKE '%' OR 1=1--%'

OR 1=1 is always true, so every row in the table comes back. More advanced payloads can use UNION SELECT to read from other tables, or call database functions that exfiltrate data out-of-band.

Fix: use parameterized queries

Pass the value as a separate argument - never interpolate it into the SQL string:

# Safe - parameterized with psycopg2
@app.get("/search")
def search():
    q = request.args.get("q", "").strip()
    cur.execute(
        "SELECT id, username, email FROM users WHERE username LIKE %s",
        (f"%{q}%",)   # the LIKE wildcards wrap the value, not the query
    )
    return jsonify(cur.fetchall())

The driver sends the SQL template and the value to the database as two separate messages. The database engine never parses the value as SQL - ' OR 1=1 -- becomes the literal search string %' OR 1=1 --%, which matches zero rows.

Why %s instead of format strings

%s is psycopg2's placeholder syntax (not Python's % string operator). When you call cur.execute(sql, params), psycopg2 handles quoting and escaping according to the PostgreSQL wire protocol, which is always correct regardless of what characters the value contains. The same pattern applies in other drivers:

Driver Placeholder
psycopg2 (PostgreSQL) %s
sqlite3 ?
PyMySQL / mysqlclient %s
SQLAlchemy ORM :name (named params)

Verify the fix

# Normal search - still works
curl "http://localhost:8000/search?q=alice"

# Injection payload - returns empty list, not all users
curl "http://localhost:8000/search?q=' OR 1=1--"

After switching to parameterized queries, static analysis tools like Bandit and semgrep stop flagging the endpoint, and a SQL injection scanner returns zero findings.

Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.

Try it in a workspace →

What you'll practice

FAQ

Why are f-string SQL queries a SQL injection risk?

An f-string embeds user input directly into the SQL text before the database parses it. An attacker who controls that input can inject SQL operators like OR 1=1, UNION SELECT, or DROP TABLE - whatever the database allows.

How do parameterized queries prevent SQL injection?

The SQL template and the user value are sent to the database separately. The database engine never parses the value as SQL, so any injected operators are treated as literal string characters and match nothing.

Does sanitizing or escaping input work instead of parameterized queries?

Not reliably. Sanitizing input with custom escaping logic is error-prone and regularly bypassed. Parameterized queries are the correct, driver-level fix - they work regardless of the input's content or encoding.

Is SQL injection still possible?

Yes - it remains common wherever code builds queries by string-concatenating user input. The fix has not changed: use parameterized queries with placeholders so input is always treated as data, never executable SQL.

Keep learning

Neutralize a Stored XSS in CommentsSecurity projectRemove Hardcoded Credentials From Source CodeSecurity projectHash Passwords Instead of Storing PlaintextSecurity projectSecurity roadmapStep by step to hiredSecurity interview questionsSTAR answersAll Security projectsProjects hub

Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.

Open the workspace →