Skip to main content
interlace
Plugin: prisma-securityRules

no-raw-identifier-interpolation

Disallow interpolating table, column or sort identifiers into a Prisma $queryRaw / $executeRaw 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 $queryRaw or $executeRaw template. This rule is part of eslint-plugin-prisma-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

Prisma ships both spellings and names one of them "Unsafe":

await prisma.$queryRawUnsafe(`SELECT * FROM ${table}`);  // flagged by no-unsafe-query
await prisma.$queryRaw`SELECT * FROM ${table}`;          // flagged by this rule

A developer who moves from the first line to the second — exactly what the Unsafe suffix tells them to do — parameterizes every value in the query and leaves the identifier hole wide open. The migration feels like a fix. Nothing in Prisma's own tooling says otherwise.

The reason is structural: a bind parameter can only ever be a value. $1 is a placeholder in the value slot of the parse tree, and no database accepts one where a table, a column, or a sort direction belongs. So when the hole is an identifier, the driver has nothing to bind and splices the string in verbatim — inside the API the docs call safe.

The remediation everyone knows, "use a parameter", is what the developer already believes they are doing, so this rule does not say it. Prisma has no identifier escaper, which leaves exactly one safe construction: map the input through a fixed allowlist, so what reaches the query is a string you wrote.

❌ Incorrect

// ❌ table name from input
await prisma.$queryRaw`SELECT * FROM ${table}`;

// ❌ column name in ORDER BY
await prisma.$queryRaw`SELECT * FROM users ORDER BY ${req.query.sort}`;

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

// ❌ $executeRaw carries the same hole
await prisma.$executeRaw`UPDATE ${table} SET active = ${flag}`;

✅ Correct

// ✅ values — exactly what the template is for
await prisma.$queryRaw`SELECT * FROM users WHERE id = ${id} LIMIT ${n}`;

// ✅ let the query builder type it — no raw SQL, nothing to get wrong
const column = ({ name: 'name', created: 'created_at' })[input] ?? 'id';
await prisma.user.findMany({ orderBy: { [column]: dir === 'desc' ? 'desc' : 'asc' } });

If you must build the SQL yourself

An allowlist makes the query safe, but it does not make the line lint-clean:

const column = ({ name: 'name', created: 'created_at' })[input] ?? 'id';
// eslint-disable-next-line prisma-security/no-unsafe-query -- column comes from a closed allowlist
await prisma.$queryRawUnsafe(`SELECT * FROM users ORDER BY ${column}`);

no-unsafe-query reports every interpolation reaching $queryRawUnsafe, and it is right to: it cannot see that column was allowlisted. The disable is the honest way to say so, and it puts the reason next to the code. Reaching for it without the allowlist above is the mistake.

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.
  • A literal. $queryRaw`SELECT * FROM ${'users'}` is a constant you typed. There is no untrusted input in it.
  • $queryRawUnsafe / $executeRawUnsafe. Those belong to no-unsafe-query, which reports every interpolation reaching them. Reporting them here as well would put two findings from one plugin on one line.

Implementation note: why there is no import gate

$queryRaw is matched on the property name alone, with no requirement that the client be imported in the same file. The Prisma client is very often re-exported from a local module —

import { prisma } from '@/lib/db';

— so a rule that demanded an @prisma/client import would miss the shape that appears in most real codebases. The $ prefix makes the property name specific enough to stand on its own.

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 prisma-security/no-raw-identifier-interpolation -- TABLE is a module constant
await prisma.$queryRaw`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.