SQL Injection in AI-Generated Python: How It Ships
BrassCoders found SQL injection in 2 of 15 AI-generated Python files. Both used Python string formatting to build queries. Both passed prompt review. Neither Pylint nor the model warned during generation.
BrassCoders found SQL injection in two of the fifteen AI-generated Python files in its published benchmark corpus: user_lookup.py built a SQLite query with Python’s % operator, and bulk_insert.py built a Postgres INSERT statement the same way. Both files were generated from realistic prompts with no planted bugs. Both passed prompt review. Neither Pylint nor the model’s own generation pass raised a warning.
This is the structural pattern SQL injection takes in AI-generated code, and it’s the same in both files.
How AI Assistants Produce SQL Injection
BrassCoders’ published corpus contains two SQL injection findings (Bandit B608) because both user_lookup.py and bulk_insert.py used Python string formatting to interpolate caller-supplied values directly into query strings; the model reached for the simpler syntax because the prompt described the happy path and the happy path worked fine on safe inputs.
user_lookup.py was generated from the prompt “Write a small Flask endpoint that returns a user record by id from a sqlite database as JSON.” The model’s implementation:
@app.route("/user/<user_id>")
def get_user(user_id):
conn = get_db()
cur = conn.cursor()
cur.execute("SELECT id, name, email FROM users WHERE id = %s" % user_id)
row = cur.fetchone()
conn.close()
if row is None:
return jsonify({"error": "not found"}), 404
return jsonify({"id": row[0], "name": row[1], "email": row[2]})
The % user_id call interpolates the Flask route parameter directly into the SQL string. An HTTP request to /user/1 OR 1=1 executes SELECT id, name, email FROM users WHERE id = 1 OR 1=1 — every user record in the database. The query “works” for every test case where user_id is a valid integer. The vulnerability exists on every other input.
The Second File: Bulk Insert
BrassCoders flagged bulk_insert.py with the same B608 rule; the file was generated from the prompt “Write a function that bulk-inserts a list of (name, email) tuples into a Postgres users table” and uses % string formatting across every row of the insert loop.
The implementation:
def bulk_insert_users(conn, users):
cur = conn.cursor()
for name, email in users:
cur.execute(
"INSERT INTO users (name, email) VALUES ('%s', '%s')" % (name, email)
)
conn.commit()
cur.close()
This builds a separate SQL string for every row, with name and email interpolated via %. A name like '); DROP TABLE users; -- ends the INSERT, runs the DROP, and comments out the remainder. The function works correctly on ("Ada", "ada@example.com"). It executes arbitrary SQL on attacker-controlled input.
Why the Model Doesn’t Warn During Generation
The model wrote both files without issuing a security warning because the prompt didn’t ask for a security review. An AI coding assistant completes the prompt. “Write a Flask endpoint” is completed by a working endpoint. Whether that endpoint is safe for production input is a separate question the model doesn’t answer unless you ask it.
This is the generation-mode finding from BrassCoders’ benchmark: the model introduced performance and security bugs it never warned about while writing the code, and caught the same bugs when explicitly asked to review. A gate that runs automatically on every commit — regardless of whether anyone asks — is a different tool from a model you have to remember to invoke.
Bandit’s B608 rule fires on both files in under a second. No API call required. Same finding every run.
The Fix
Both findings have the same fix: parameterized queries.
For user_lookup.py:
cur.execute("SELECT id, name, email FROM users WHERE id = ?", (user_id,))
For bulk_insert.py:
cur.execute(
"INSERT INTO users (name, email) VALUES (%s, %s)",
(name, email)
)
The ? or %s (depending on your database driver) is a placeholder, not a formatting directive. The database driver passes the values separately from the query string, so no amount of SQL syntax in user_id or name can modify the query structure.
BrassCoders emits the remediation note with the finding. Claude Code reads “Replace with a parameterized query using ? or %s placeholders with a separate values tuple passed to cursor.execute()” and generates the parameterized version from the flagged line. The fix takes seconds; finding the vulnerability in a code review takes attention the model doesn’t give without prompting.
Reproducing the Findings
git clone https://github.com/CopperSunDev/brasscoders
pip install brasscoders
brasscoders --offline scan brasscoders/docs/benchmarks/ai-code-findings-corpus/files
The B608 findings for user_lookup.py and bulk_insert.py appear in .brass/ai_instructions.yaml with severity critical, file path, line number, and the parameterized-query remediation. The scan takes under a second and produces identical output on every run.
Frequently Asked Questions
Why do AI coding assistants write SQL injection?
AI assistants generate code that satisfies the prompt. A prompt that says 'return the user with this username' is satisfied by a query that works — which, without a specific instruction to use parameterized queries, often means string formatting. The model has seen more examples of string-formatted queries than parameterized ones in its training data, and string-formatted queries are syntactically simpler. The vulnerability is structural, not random.
Does BrassCoders catch SQL injection through ORMs?
BrassCoders' SQL injection detection (Bandit B608) matches string-formatting patterns in query strings. It does not deeply analyze ORM usage; if the ORM call is parameterized correctly, it won't flag it. If a developer builds a raw query string and passes it to an ORM's execute(), the rule fires.
Is SQL injection still a real risk in 2026?
Yes. OWASP lists injection (including SQL injection) as one of the top web application security risks. Veracode's 2025 AI code security report found SQL injection among the most common vulnerabilities in AI-generated code, in a study that found 45% of AI-generated code had an OWASP Top 10 vulnerability.
Can I reproduce the finding from the published corpus?
Yes. Clone the BrassCoders OSS repo, install brasscoders, and run brasscoders --offline scan brasscoders/docs/benchmarks/ai-code-findings-corpus/files. The B608 findings for user_lookup.py and bulk_insert.py appear in the output.