Skip to main content

No SQL string concatenation

ab-002575 · project-snapshot.injection.no-sql-string-concat
Severity: highactive

Why it matters

db.query(\SELECT * FROM users WHERE id = ${userId}`)is textbook SQL injection — any attacker who controlsuserIdcan read arbitrary tables, dump the full user table, or drop tables entirely depending on driver permissions. Parameterized queries exist in every database driver and are not harder to write; the interpolated form just pattern-matches onto familiar JavaScript template-literal syntax, which is why AI coding tools produce it constantly. Prisma's tagged-template$queryRawis safely parameterized but its sibling$queryRawUnsafe` looks nearly identical and isn't — tools grab whichever completes first. Even when the current caller passes a "trusted" value, the pattern invites the next contributor to pipe a request parameter through the same path. The check catches the shape before the exploit.

Severity rationale

High because classical SQL injection is a one-query-away full-database compromise, but the attack requires a user-controlled code path reaching the interpolation — not every interpolated query is immediately exploitable in the current caller.

Remediation

Use parameterized queries:

// postgres.js / pg
await db.query('SELECT * FROM users WHERE id = $1', [userId])
// Prisma raw
await prisma.$queryRaw`SELECT * FROM users WHERE id = ${userId}`  // tagged template = parameterized

Deeper remediation guidance and cross-reference coverage for this check lives in the security-hardening Pro audit — run that after applying this fix for a more exhaustive pass on the same topic.

Detection

  • ID: project-snapshot.injection.no-sql-string-concat
  • Severity: high
  • What to look for: Enumerate every SQL query in the codebase. Patterns: .query(\SELECT...${var}`)(template literal interpolation),.query("SELECT..." + var)(string concatenation),.raw(`...${var}`), db.execute(`...${var}`), prisma.$queryRawUnsafe(`...${var}`)`. Count interpolated/concatenated queries vs. parameterized ones.
  • Pass criteria: Zero queries built via string interpolation or concatenation. All dynamic queries use parameterized placeholders (?, $1, :name) or a query-builder API.
  • Fail criteria: At least one query with interpolated user-controlled data.
  • Skip (N/A) when: Project uses an ORM that doesn't expose raw SQL paths (e.g., pure Prisma without $queryRaw/$queryRawUnsafe usage). Quote the ORM detected.
  • Do NOT pass when: A query interpolates a "trusted" constant — the pattern is dangerous regardless of current data source. Static constants are fine; refactor anyway.
  • Before evaluating, quote: Quote the line for any matched interpolation pattern.
  • Report even on pass: "Scanned N query call sites; 0 use string interpolation."
  • Detail on fail: "src/lib/users.ts:42 builds query via template literal: db.query(\SELECT * FROM users WHERE id = ${userId}`)"`.
  • Remediation: Use parameterized queries:
    // postgres.js / pg
    await db.query('SELECT * FROM users WHERE id = $1', [userId])
    // Prisma raw
    await prisma.$queryRaw`SELECT * FROM users WHERE id = ${userId}`  // tagged template = parameterized
    

Taxons

History