SQL Injection in AI-Generated Code, by the Numbers

Veracode found 1 in 5 AI-generated SQL snippets still vulnerable, and a 2022 study put Copilot's SQL injection rate at 37%. The numbers, and how to catch it.

BrassCoders Team · · 6 min read
securitybenchmarks

One in five. That’s how many AI-generated code snippets that touch a database still ship a SQL injection hole, according to Veracode’s 2025 testing of more than 100 language models. SQL was the category the models handled best. They still failed it a fifth of the time.

SQL injection is not an exotic bug. It’s CWE-89, ranked third on the 2024 CWE Top 25, and the flagship weakness in Injection, the category that sits third on the OWASP Top 10. It has a fix that every major web framework ships by default. Your AI assistant writes the vulnerable form anyway, because a string-built query returns the right rows in testing and only breaks when a stranger sends a quote character.

The Numbers on AI-Generated SQL Injection

BrassCoders treats SQL injection as a measured risk in AI-generated code, not a hypothetical. Veracode’s 2025 testing found AI models pass SQL-handling security checks 80% of the time, which leaves one in five database snippets vulnerable, and a 2022 IEEE study measured a 37.35% vulnerability rate across GitHub Copilot’s SQL-related completions.

Veracode’s 2025 GenAI Code Security Report ran more than 100 models across 80 coding tasks in four languages and four vulnerability classes. Across the whole set, 45% of the generated code failed security tests and introduced an OWASP Top 10 weakness. SQL injection was the class the models handled best. Veracode still reported an 80% security pass rate on that task, and called the remaining 20% a significant risk for database-driven applications.

The academic record runs further back. In Asleep at the Keyboard?, a 2022 IEEE Symposium on Security and Privacy paper, researchers generated 1,689 Copilot programs across 89 scenarios and found roughly 40% of them vulnerable. When they narrowed to SQL injection and varied the prompt 17 ways, 152 of 407 valid programs carried the flaw. That’s 37.35%. They chose CWE-89 for the experiment because its verdict is unambiguous: a query is either open to injection or it isn’t, with no grey zone to argue about.

Injection didn’t earn its ranking by accident. OWASP’s A03:2021 category reports that 94% of tested applications were probed for some form of injection, and MITRE’s CWE-89 entry places it in the Top 25 most dangerous weaknesses. The full evidence set, with every source, sits in BrassCoders’s SQL injection research index.

Why AI Assistants Reach for String-Built SQL

BrassCoders treats string-built SQL as a structural symptom of how a model generates code, not a rare slip. Asked for a function that looks up a user by email, the model aims for a query that returns the right row on the developer’s test input, and dropping the email straight into the query string is the shortest path to that goal.

Consider the two ways to write the same lookup. The vulnerable form is one line:

cursor.execute(f"SELECT * FROM users WHERE email = '{email}'")

The safe form is barely longer, and it hands the value to the driver separately from the SQL text:

cursor.execute("SELECT * FROM users WHERE email = ?", (email,))

Both return the same row for alice@example.com. Only the first one also returns every row in the table when someone sends ' OR '1'='1. Nothing in the prompt rewarded the second version, and nothing in the developer’s quick test exercised the input that separates them. This is the same gap that produces every other class of AI-generated bug: the model satisfies the stated goal, and the adversarial input was never part of the goal.

What Static Analysis Catches

BrassCoders bundles Bandit, and Bandit ships a dedicated check, B608, for exactly this shape. As one of the 12 scanners in a BrassCoders scan, it flags the pattern before the query ever runs against real input.

Bandit’s B608 documentation describes the check plainly: it looks for strings that resemble SQL statements involved in some form of string-building operation. The f-string lookup above matches that description exactly. So does a query assembled with + concatenation or % formatting, or one built up across several lines before it reaches cursor.execute. A static rule catches the structural marker, the SQL keywords sitting next to a string operation, without executing anything.

That structural detection is also the boundary of what a scanner can know on its own.

What Static Analysis Can’t Decide

BrassCoders reports the raw B608 pattern match and stops there. Whether the interpolated value is actually attacker-controlled, reachable from an untrusted request, or already escaped upstream is a source-context judgment BrassCoders leaves to the AI assistant reading its YAML output rather than inferring itself.

The distinction matters because B608 fires on shape, not on reachability. A query built from a hardcoded table name the developer controls is a false positive. The identical pattern built from request.args.get("email") is a live vulnerability. A deterministic scanner that tried to tell those apart would have to trace the value back through the call graph and decide whether the source is trusted, and if it guessed wrong, it would either bury a real bug or cry wolf on a safe one. BrassCoders emits the finding with its file, line, and type; Claude Code or Cursor reads the surrounding code and decides whether this specific match is exploitable. BrassCoders is the pattern reporter and the AI assistant is the triage layer, the same division of labor a BrassCoders scan applies to every finding class.

The Fix the Framework Already Ships

BrassCoders points every SQL injection finding back to the fix the language and framework vendors already document: parameterized queries. The safe form hands user input to the database driver as data, so it can never be read as SQL.

Python’s own sqlite3 documentation warns against assembling queries with string operations and tells developers to always use placeholders to bind values, precisely so an attacker can’t close a quote and inject their own clause.

Frameworks make the safe path the default. Django’s security documentation explains that its querysets are protected from SQL injection because the query’s SQL is defined separately from its parameters, and the database driver escapes anything user-provided. The risk returns the moment generated code steps outside the ORM to write raw SQL, which is exactly what an assistant does when a prompt asks for a query the ORM makes awkward. That’s the line worth a second look on any AI-written data-access change: not whether a query exists, but whether it was built by hand out of strings.

Install BrassCoders and get Bandit’s B608 check running as one of 12 scanners on every commit: pip install brasscoders.

Frequently Asked Questions

How often does AI generate SQL injection bugs?

Veracode's 2025 testing of more than 100 language models found an 80% pass rate on SQL-handling tasks, meaning one in five AI-generated database snippets still shipped a SQL injection flaw, and SQL was the category the models handled best. A 2022 IEEE Symposium on Security and Privacy study of GitHub Copilot measured a higher rate: 152 of 407 valid SQL-related completions, 37.35%, were vulnerable. Both numbers describe generated code before any human review.

What is SQL injection, formally?

SQL injection is CWE-89 in MITRE's Common Weakness Enumeration: code constructs an SQL command from externally-influenced input without neutralizing the special characters that can change the command's meaning. The result is that attacker-supplied text gets interpreted as SQL instead of as ordinary data. It ranks third on the 2024 CWE Top 25, and its OWASP Top 10 2021 category, Injection, also sits third, appearing in 94% of tested applications.

Why do AI assistants write vulnerable SQL when the fix is well known?

An AI assistant asked to look up a user aims for a query that returns the right row on the example input. An f-string that drops a variable straight into the SQL text does that in one line and passes every test a developer would try. Parameterization adds a step the prompt never asked for, so the model reaches for the shorter form that only breaks once a stranger controls the input.

How do I catch SQL injection in AI-generated code before it merges?

Run a static analyzer that flags string-built SQL on every pull request. Bandit's B608 check looks for strings that resemble SQL statements assembled through string operations, without needing to run the query. Pair that with the framework fix the language vendors already document: parameterized queries with placeholders, which separate the SQL text from the user-supplied values so the values can never be read as code.

Does BrassCoders detect SQL injection?

BrassCoders bundles Bandit, which ships the B608 check for string-built SQL, as one of its 12 scanners. BrassCoders reports the raw pattern match; whether the interpolated value is actually reachable from an untrusted request, or already sanitized upstream, is the source-context judgment BrassCoders leaves to the AI assistant reading its YAML output rather than inferring itself.