# How to Fix AI Writing Insecure SQL

> AI agents build SQL queries with string interpolation instead of parameterized statements, introducing SQL injection vulnerabilities into production database code.

**Type:** Failure  
**Tools:** Cursor, Claude Code, Codex, Windsurf  
**Stack:** PostgreSQL, TypeScript  
**Updated:** 2026-06-08

---

The agent reaches for template literals to build SQL queries, creating classic
SQL injection vectors that appear to work fine in tests but are exploitable in
production.

## The symptom

User-controlled values are interpolated directly into a SQL string.

```ts
// WRONG — SQL injection vulnerability
async function getUserByEmail(email: string) {
  const result = await db.query(
    `SELECT * FROM users WHERE email = '${email}'`
    //                                  ^^^^^^^ attacker-controlled
  );
  return result.rows[0];
}

// Attacker input: ' OR '1'='1
// Resulting query: SELECT * FROM users WHERE email = '' OR '1'='1'
// Returns every row in the table.
```

## Why it happens

Template literals are the most natural string-building tool in JavaScript, and
many tutorial examples the model trained on use them for SQL without
parameterization. The agent also doesn't model adversarial inputs — it imagines
well-formed data passing through.

## How to spot it

- SQL strings that contain `${...}` interpolations.
- Query functions that accept user input and pass it to a raw query helper
  without a separate params array.
- `db.query(sql)` called with a single argument instead of `db.query(sql, [params])`.
- `LIKE '%${term}%'` patterns.

## How to fix it

Always use parameterized queries. The database driver handles escaping; your
code never touches quoting.

```ts
// CORRECT — parameterized query (node-postgres / pg)
async function getUserByEmail(email: string) {
  const result = await db.query(
    "SELECT id, name, email FROM users WHERE email = $1",
    [email]  // second argument: params array
  );
  return result.rows[0] ?? null;
}

// CORRECT — with Postgres.js (template tag)
async function searchUsers(term: string) {
  return sql`SELECT id, name FROM users WHERE name ILIKE ${"%" + term + "%"}`;
  // postgres.js automatically parameterizes template expressions
}
```

```txt
[ ] No ${...} inside raw SQL strings — use $1/$2 placeholders instead
[ ] Every db.query() call passes user input via the params array, not the SQL string
[ ] Use an ORM (Prisma, Drizzle) or query builder for complex queries
[ ] LIKE wildcards are appended in the param value, not concatenated into the SQL
[ ] Run sqlfluff or a SQL linter in CI to catch interpolated strings
```

## Fix Prompt

```txt title="Fix Prompt"
This SQL query uses string interpolation with user-supplied values, which is a
SQL injection vulnerability. Rewrite every raw query to use parameterized
statements ($1, $2 placeholders for pg, or the tagged template literal form for
postgres.js). Never interpolate variables directly into SQL strings. If the
query is complex, migrate it to Prisma or Drizzle ORM instead.
```

## Test

```bash
# Detect template literal interpolation inside SQL-looking strings
grep -rn 'query(`\|sql`\|execute(`' --include="*.ts" --include="*.tsx" . \
  | grep '\${' \
  | grep -v "node_modules" \
  && echo "FAIL: interpolated SQL found" || echo "OK"
```