How to Prevent SQL Injection in AI-Generated Code
AI coding tools move fast — but they often generate SQL queries without parameterization. Learn how to detect and fix SQL injection vulnerabilities before they reach production.
By ShipReady · Updated
AI coding assistants are remarkably good at turning "let me fetch users by email" into working code in seconds. They are also remarkably good at building that query by gluing strings together — which is exactly how SQL injection happens. Nearly two decades after it topped the OWASP list, injection is still one of the most common and most damaging vulnerabilities on the web, and AI-generated code has quietly made it more common again.
Why AI-generated code is especially prone
Large language models pattern-match against their training data, and a huge amount of tutorial code online builds queries with string interpolation because it reads cleanly in a blog post. That code "works" in a demo, so it survives. The model has no concept of the attacker who will later pass a single quote in a form field.
The one rule: parameterize every query
The fix is not clever escaping. It is to never build SQL by concatenating user input. Use parameterized queries (also called prepared statements), where the driver sends the query and the values separately and the database never treats input as executable SQL.
// ❌ Vulnerable — user input becomes part of the SQL
const q = `SELECT * FROM users WHERE email = '${email}'`
await db.query(q)
// ✅ Safe — the value is bound as a parameter, never parsed as SQL
await db.query('SELECT * FROM users WHERE email = $1', [email])What parameterisation does not cover
One limit worth knowing, because it is where people who "always parameterise" still get caught. A placeholder can only stand in for a value. It cannot stand in for a table name, a column name, or the direction of an ORDER BY — those are part of the query structure, and the driver has no way to bind them.
// ❌ Still injectable — the column is structure, not a value
await db.query(`SELECT * FROM users ORDER BY ${sortBy} ${direction}`)
// ✅ Allowlist the structure, parameterise the values
const COLUMNS = { name: 'name', created: 'created_at' }
const column = COLUMNS[sortBy] ?? 'created_at'
const dir = direction === 'asc' ? 'ASC' : 'DESC'
await db.query(`SELECT * FROM users ORDER BY ${column} ${dir} LIMIT $1`, [limit])The pattern is an allowlist that maps user input to a fixed set of known-good strings. Never sanitise a column name — decide it.
Defense in depth
Parameterization stops the vulnerability. These practices limit the blast radius if something slips through:
- Give the application database user the least privilege it needs — no DROP, no access to tables it never reads.
- Validate and allowlist input shapes where you can (an email looks like an email, an id is an integer).
- Prefer a query builder or ORM that parameterizes by default over hand-written SQL strings.
- Turn off verbose database errors in production so a probe cannot read your schema from an error message.
What an outside scan can and cannot tell you
Be clear about the limits here, because a lot of tooling is not. Injection is a property of source code. Proving it from outside means sending crafted payloads at a live database, which is an intrusive test you should only run against a target you own and have scheduled. ShipReady does not do that — it is a passive scanner, and it reports only what it can observe without attacking the site.
What a passive scan does catch is the surrounding failure that makes an injection bug far worse: a database connection string exposed in a public file, Supabase tables with row-level security disabled so the client can read them with no injection required at all, and a stack trace disclosed to visitors that hands over your schema and driver version for free. Those are the conditions that turn a probe into a breach, and they are visible from the outside.
So: parameterize in code, review every data-access path your assistant writes, and use a scan to confirm none of the amplifiers are live. The security risks of AI-generated code covers the wider pattern, and the production readiness checklist is the task-ordered version of the same ground.