Skip to main content
interlace
Plugin: drizzle-securityRules

no-raw-identifier-interpolation

Disallow interpolating table, column or sort identifiers into a Drizzle `sql` tagged template, where bind parameters cannot reach them.

CWE: CWE-89 OWASP: A03:2021 – Injection

Detects an identifier — a table, a column, a sort direction — interpolated into a sql`…` template. This rule is part of eslint-plugin-drizzle-security.

💼 This rule is set to error in the strict config.

Quick Summary

AspectDetails
CWE ReferenceCWE-89 (SQL Injection)
SeverityCritical (CVSS 9.8)
Auto-Fix❌ No auto-fix available
CategorySecurity

Why this matters

sql`…` parameterizes. That is the entire reason to use it, and it is why these two lines get the same review:

await db.execute(sql`SELECT * FROM users WHERE id = ${id}`);  // safe
await db.execute(sql`SELECT * FROM ${table}`);                // injectable

The first is safe. The second is not, and no amount of care with the template changes that, because a bind parameter can only ever be a value. $1 is a placeholder in the value slot of the parse tree. No database accepts one where a table, a column, or a sort direction belongs — so when you put an identifier hole there, the driver has nothing to bind and splices your string in verbatim.

This is the shape behind GHSA-gpj5-g38j-94v9. It is invisible to every SQL-injection linter that decides by asking "is this a raw API", because this is the safe API.

The reason it survives review is that the remediation everyone knows — "use a parameter" — is what the developer already believes they are doing. Repeating it produces the loop the vulnerability came from. There are only two real fixes, and this rule's messages name them: Drizzle's sql.identifier(), or an allowlist.

❌ Incorrect

import { sql } from 'drizzle-orm';

// ❌ the advisory shape — table name from input
await db.execute(sql`SELECT * FROM ${table}`);

// ❌ column name in ORDER BY
await db.execute(sql`SELECT * FROM users ORDER BY ${req.query.sort}`);

// ❌ sort direction — two legal values, and neither is bindable
await db.execute(sql`SELECT * FROM users ORDER BY name ${dir}`);

// ❌ pre-quoting escapes nothing; it only makes the line look deliberate
await db.execute(sql`SELECT * FROM "${table}"`);

✅ Correct

import { sql } from 'drizzle-orm';

// ✅ values — exactly what the template is for
await db.execute(sql`SELECT * FROM users WHERE id = ${id} LIMIT ${n}`);

// ✅ the escaper quotes the identifier properly
await db.execute(sql`SELECT * FROM ${sql.identifier(table)}`);

// ✅ an allowlist turns input into a value you wrote
const column = { name: 'name', created: 'created_at' }[input] ?? 'id';
await db.execute(sql`SELECT * FROM users ORDER BY ${sql.identifier(column)}`);

// ✅ a direction resolved to a literal
const dir = input === 'desc' ? 'desc' : 'asc';

What this rule deliberately does not report

  • Every value position. WHERE id = ${id}, LIMIT ${n}, VALUES (${name}), SET a = ${v} are what the tagged template parameterizes correctly. A rule that fired on the API's intended use is a rule that gets switched off.
  • Drizzle's composition surfacesql.identifier(), sql.join(), sql.fromList(), sql.placeholder(). These produce SQL chunks rather than spliced text, and sql.identifier() in particular is the fix this rule recommends; reporting the remediation punishes the correction. sql.raw() is pointedly not in that list — it is the one member of the family that does splice, so sql`SELECT * FROM ${sql.raw(table)}` is still a finding here.
  • A literal. sql`SELECT * FROM ${'users'}` is a constant you typed. There is no untrusted input in it.
  • A nested sql`…` fragment. Composition is Drizzle's intended primitive, and the nested template is checked on its own visit — so its holes are still covered, just not twice.
  • A file that never imports drizzle-orm. sql is far too common a local name to key on alone; the driver import is the gate that keeps this rule inside its own plugin.

How this splits with no-unsafe-query

The two rules divide by what is wrong, not by which API you used:

reports
no-unsafe-querystring construction — concatenation or interpolation used to build the SQL text passed to sql.raw(...)
this ruleposition — a hole where an identifier belongs, inside the tagged template

So sql`SELECT * FROM ${sql.raw(table)}` is a finding here: the hole is in an identifier position and sql.raw() splices rather than composing. sql.raw('SELECT * FROM ' + table) is a finding there: the string is built by concatenation. A line that does both is two different defects with two different fixes, not one finding reported twice.

When Not To Use It

There is no configuration where interpolating an identifier into a query is correct, so this rule has no options — which SQL positions accept a bind parameter is fixed by the database's grammar, not by project preference.

If a specific line is genuinely a constant the analyzer cannot see through, disable it on that line with a reason rather than switching the rule off:

// eslint-disable-next-line drizzle-security/no-raw-identifier-interpolation -- TABLE is a module constant
await db.execute(sql`SELECT * FROM ${TABLE}`);

Further Reading

Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.