SQL injection in AI-generated code: the pattern LLMs keep repeating
Par Seaworthy · 12 juin 2026
Photo by Diana Polekhina on Unsplash
SQL injection is a 1990s vulnerability. We have known about it for decades, written countless tutorials, included it in every security course ever made. And yet, in 2024, it is showing up in AI-generated code at a rate that should alarm anyone paying attention.
This is not a failure of the developers using these tools. It is a property of how LLMs generate code.
Why LLMs default to string concatenation
When an LLM produces a SQL query, it is predicting the most statistically likely next token given the context. Training data contains a lot of old tutorials, Stack Overflow answers from 2009, and blog posts that were written before parameterisation was widely taught. That code uses string concatenation. So the model has seen this pattern far more than it has seen ? placeholders or named parameters.
There is also a readability effect. String interpolation looks like the query you are trying to write. It is easy to read in the training context. The model produces code that looks correct and clear at a glance.
The result: if you ask an LLM to "write a query that fetches a user by email", there is a meaningful chance it gives you this:
query = "SELECT * FROM users WHERE email = '" + email + "'"
cursor.execute(query)
Or its f-string equivalent, which is just as bad:
cursor.execute(f"SELECT * FROM users WHERE email = '{email}'")
Both are directly injectable. If email comes from user input and you have not sanitised it upstream, an attacker sends ' OR '1'='1 and gets your entire users table.
Spotting it in a review
The pattern is mechanical to detect. You are looking for any SQL string that contains a variable, and where that variable is inserted via concatenation, f-string interpolation, or .format(). In Python that is f"SELECT...", "SELECT..." + var, or "SELECT...".format(...). In JavaScript it is template literals: `SELECT * FROM users WHERE id = ${userId}`.
The safe version looks like this:
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))
Or with named parameters depending on your driver:
cursor.execute("SELECT * FROM users WHERE email = :email", {"email": email})
The query string is static. The values are passed separately. The database driver handles escaping in a way that cannot be subverted by clever input formatting.
This is not hard to write. The LLM just does not always produce it without being told to.
The fix is a prompt nudge, but verify
If you are generating queries with an LLM, you can push it toward safe output by being explicit in your prompt: "use parameterised queries, do not use f-strings or string concatenation for SQL values." That helps. The model will usually comply.
But verify. Read the generated code. If you see a query string with a variable inside it, assume it is unsafe until you have confirmed the driver is handling parameterisation. This is one of those cases where "looks right" and "is right" diverge badly.
ORM pitfalls: you are not automatically safe
A lot of developers assume that using an ORM sidesteps this whole problem. Sometimes it does. Often it does not.
The classic trap is raw query escape hatches. SQLAlchemy has text(). Django has raw(). Prisma has queryRaw. These exist for a reason, and when you use them, you are back to the same problem. LLMs reach for these methods when they cannot figure out how to express a query through the ORM's fluent interface, which happens often with complex joins or database-specific functions.
# This is not safe
result = db.execute(text(f"SELECT * FROM orders WHERE status = '{status}'"))
# This is safe
result = db.execute(text("SELECT * FROM orders WHERE status = :status"), {"status": status})
SQLAlchemy's text() supports bound parameters. Use them.
Django's raw() has the same dual nature:
# Unsafe
User.objects.raw(f"SELECT * FROM users WHERE name = '{name}'")
# Safe
User.objects.raw("SELECT * FROM users WHERE name = %s", [name])
There is another ORM footgun worth mentioning: order_by with user-supplied column names. Most ORMs do not parameterise column names or table names, only values. If you are letting a user choose the sort column and passing that directly to your ORM's order clause, you may have an injection vector even if you are using the ORM's "safe" API. Allowlist the column names explicitly.
A note on stored procedures
Stored procedures are sometimes pitched as a mitigation. They can be, if written correctly. A stored procedure that constructs SQL strings dynamically inside the database using EXEC or EXECUTE IMMEDIATE with concatenated user input is still injectable. The code just moved. This is relevant because LLMs, when asked to write stored procedures, can produce exactly this pattern.
What this actually looks like at scale
If your codebase has twenty query functions and you wrote them all with AI assistance, there is a reasonable chance at least a few of them use concatenation. The LLM is not malicious, it is just inconsistent. It will write ten safe queries and two unsafe ones, and you may not notice without a systematic review.
The uncomfortable reality is that static analysis catches this reliably. Human review catches it when you are lucky and paying close attention. "Looks fine to me" is not sufficient for SQL queries that touch user input.
Seaworthy flags this pattern automatically as part of its security checks.
Cet article a été généré par IA et résume des sources publiquement disponibles.
Seaworthy analyse votre dépôt pour détecter les problèmes abordés dans des articles comme celui-ci.
Failles de sécurité, secrets exposés et mauvaises configurations détectés avant le déploiement. Gratuit, aucun compte requis.