{
  "id": "ai-writes-insecure-sql",
  "type": "failures",
  "category": "failures",
  "locale": "en",
  "url": "/failures/ai-writes-insecure-sql",
  "title": "How to Fix AI Writing Insecure SQL",
  "description": "AI agents build SQL queries with string interpolation instead of parameterized statements, introducing SQL injection vulnerabilities into production database code.",
  "tools": [
    "Cursor",
    "Claude Code",
    "Codex",
    "Windsurf"
  ],
  "stack": [
    "PostgreSQL",
    "TypeScript"
  ],
  "tags": [
    "sql",
    "security",
    "postgres"
  ],
  "difficulty": null,
  "updated": "2026-06-08",
  "markdown": "The agent reaches for template literals to build SQL queries, creating classic\nSQL injection vectors that appear to work fine in tests but are exploitable in\nproduction.\n\n## The symptom\n\nUser-controlled values are interpolated directly into a SQL string.\n\n```ts\n// WRONG — SQL injection vulnerability\nasync function getUserByEmail(email: string) {\n  const result = await db.query(\n    `SELECT * FROM users WHERE email = '${email}'`\n    //                                  ^^^^^^^ attacker-controlled\n  );\n  return result.rows[0];\n}\n\n// Attacker input: ' OR '1'='1\n// Resulting query: SELECT * FROM users WHERE email = '' OR '1'='1'\n// Returns every row in the table.\n```\n\n## Why it happens\n\nTemplate literals are the most natural string-building tool in JavaScript, and\nmany tutorial examples the model trained on use them for SQL without\nparameterization. The agent also doesn't model adversarial inputs — it imagines\nwell-formed data passing through.\n\n## How to spot it\n\n- SQL strings that contain `${...}` interpolations.\n- Query functions that accept user input and pass it to a raw query helper\n  without a separate params array.\n- `db.query(sql)` called with a single argument instead of `db.query(sql, [params])`.\n- `LIKE '%${term}%'` patterns.\n\n## How to fix it\n\nAlways use parameterized queries. The database driver handles escaping; your\ncode never touches quoting.\n\n```ts\n// CORRECT — parameterized query (node-postgres / pg)\nasync function getUserByEmail(email: string) {\n  const result = await db.query(\n    \"SELECT id, name, email FROM users WHERE email = $1\",\n    [email]  // second argument: params array\n  );\n  return result.rows[0] ?? null;\n}\n\n// CORRECT — with Postgres.js (template tag)\nasync function searchUsers(term: string) {\n  return sql`SELECT id, name FROM users WHERE name ILIKE ${\"%\" + term + \"%\"}`;\n  // postgres.js automatically parameterizes template expressions\n}\n```\n\n```txt\n[ ] No ${...} inside raw SQL strings — use $1/$2 placeholders instead\n[ ] Every db.query() call passes user input via the params array, not the SQL string\n[ ] Use an ORM (Prisma, Drizzle) or query builder for complex queries\n[ ] LIKE wildcards are appended in the param value, not concatenated into the SQL\n[ ] Run sqlfluff or a SQL linter in CI to catch interpolated strings\n```\n\n## Fix Prompt\n\n```txt title=\"Fix Prompt\"\nThis SQL query uses string interpolation with user-supplied values, which is a\nSQL injection vulnerability. Rewrite every raw query to use parameterized\nstatements ($1, $2 placeholders for pg, or the tagged template literal form for\npostgres.js). Never interpolate variables directly into SQL strings. If the\nquery is complex, migrate it to Prisma or Drizzle ORM instead.\n```\n\n## Test\n\n```bash\n# Detect template literal interpolation inside SQL-looking strings\ngrep -rn 'query(`\\|sql`\\|execute(`' --include=\"*.ts\" --include=\"*.tsx\" . \\\n  | grep '\\${' \\\n  | grep -v \"node_modules\" \\\n  && echo \"FAIL: interpolated SQL found\" || echo \"OK\"\n```"
}