AI writes f"SELECT * FROM users WHERE id={user_id}" because that's what gets upvoted on Stack Overflow. It works. It's readable. It's also a textbook SQL injection.
This isn't a hypothetical. I found this exact pattern in a UK Government repo with 2,693 stars. Production code. Merged, reviewed, deployed.
The Pattern
Here's what AI-generated SQL injection looks like in the wild:
# What AI writes
def get_user(user_id):
query = f"SELECT * FROM users WHERE id={user_id}"
cursor.execute(query)
return cursor.fetchone()
# What it should write
def get_user(user_id):
query = "SELECT * FROM users WHERE id=?"
cursor.execute(query, (user_id,))
return cursor.fetchone()
One character difference in the call. Completely different security posture.
The parameterized version treats user_id as data. The f-string version treats it as code. Pass 1; DROP TABLE users-- and the first version executes it.
Why AI Defaults to F-strings
Asked Claude, GPT-4, and Copilot to "write a function that queries users by ID." All three used f-strings on the first try.
Why? Because their training data is full of tutorials, blog posts, and Stack Overflow answers that use f-strings. Tutorial code optimizes for clarity, not security. The model learned what gets upvoted.
The fix exists in the training data too. Parameterized queries are well-documented. But the model has to actively choose the more verbose pattern over the simpler one. Without security context, it picks simple.
Real Examples
UK Government inspect_ai (2,693 stars)
An AI safety evaluation framework. The irony writes itself.
# src/inspect_ai/_display/textual/app.py:307
query = f"SELECT * FROM results WHERE {filter}"
filter comes from user input. No sanitization. No parameterization. Classic.
goldenmatch (131 stars)
A matching library:
# materialize.py:58
query = f"INSERT INTO matches VALUES ('{name}', '{score}')"
cursor.execute(query)
String concatenation inside SQL. Pass '; DROP TABLE matches; -- as a name and you own the database.
Same file, line 227:
query = f"SELECT * FROM {table} WHERE id = '{match_id}'"
Two injection points in the same file.
The Variants
F-string SQL injection shows up in several forms. All dangerous, all common in AI code:
Direct f-string
cursor.execute(f"SELECT * FROM users WHERE id={user_id}")
String concatenation
query = "SELECT * FROM users WHERE id=" + user_id
cursor.execute(query)
Format method
query = "SELECT * FROM users WHERE id={}".format(user_id)
cursor.execute(query)
Percent formatting
query = "SELECT * FROM users WHERE id=%s" % user_id
cursor.execute(query)
All four do the same thing: embed untrusted data directly into a SQL string. AI generates all four variants.
The Fix Takes 10 Seconds
Every Python database library supports parameterized queries:
# sqlite3
cursor.execute("SELECT * FROM users WHERE id=?", (user_id,))
# psycopg2 (PostgreSQL)
cursor.execute("SELECT * FROM users WHERE id=%s", (user_id,))
# mysql-connector
cursor.execute("SELECT * FROM users WHERE id=%s", (user_id,))
# SQLAlchemy
session.execute(text("SELECT * FROM users WHERE id=:id"), {"id": user_id})
If you're using an ORM (Django, SQLAlchemy), the ORM handles parameterization for you. The only time you hit this bug is when writing raw SQL, which is exactly when AI reaches for f-strings.
How to Catch It
Manual review
Look for any cursor.execute(), db.execute(), or session.execute() call where the argument contains f", +, .format(, or %. If the string has user-controlled variables, it's injectable.
Automated
pip install aiverify
aiverify your_project/ --rules sql_injection
AIVerify checks for f-strings and string concatenation in SQL calls, but filters out false positives like test files and static queries with no user input.
Pre-commit hook
Catch it before it reaches the repo:
# .pre-commit-config.yaml
- repo: https://github.com/turingrtss/aiverify
hooks:
- id: aiverify
The Bigger Problem
SQL injection is just one pattern. AI also generates:
-
Command injection:
subprocess.run(f"git clone {url}", shell=True) -
SSRF:
requests.get(user_provided_url)with no validation - Hardcoded secrets: API keys that look like placeholders but aren't
-
Path traversal:
open(f"/data/{user_filename}")with no sanitization
Each of these deserves its own post. The common thread: AI writes code that works on the happy path and breaks catastrophically on the adversarial path.
Takeaway
If you're reviewing AI-generated code, ctrl+F for f" near any database call. That single search will catch 80% of SQL injection bugs AI introduces.
If you want automated coverage, run a scanner. AIVerify, Bandit, Semgrep, whatever. The specific tool matters less than running something.
AI isn't going to stop writing f-string SQL. It's on us to catch it.
GitHub: https://github.com/turingrtss/aiverify
Previous post: How I Found 12 Critical Security Bugs in AI-Generated Code in 24 Hours
Top comments (0)