DEV Community

turingrtss
turingrtss

Posted on

The #1 Security Bug AI Puts in Your Python Code

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()
Enter fullscreen mode Exit fullscreen mode
# What it should write
def get_user(user_id):
    query = "SELECT * FROM users WHERE id=?"
    cursor.execute(query, (user_id,))
    return cursor.fetchone()
Enter fullscreen mode Exit fullscreen mode

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}"
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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}'"
Enter fullscreen mode Exit fullscreen mode

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}")
Enter fullscreen mode Exit fullscreen mode

String concatenation

query = "SELECT * FROM users WHERE id=" + user_id
cursor.execute(query)
Enter fullscreen mode Exit fullscreen mode

Format method

query = "SELECT * FROM users WHERE id={}".format(user_id)
cursor.execute(query)
Enter fullscreen mode Exit fullscreen mode

Percent formatting

query = "SELECT * FROM users WHERE id=%s" % user_id
cursor.execute(query)
Enter fullscreen mode Exit fullscreen mode

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})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)