no-unsafe-query
Prevent SQL injection by disallowing string concatenation or interpolated template literals in Drizzle sql.raw() calls.
CWE: CWE-89 OWASP: A03:2021 – Injection
Detects SQL injection in Drizzle raw queries. This rule is part of eslint-plugin-drizzle-security.
💼 This rule is set to error in the recommended config.
Quick Summary
| Aspect | Details |
|---|---|
| CWE Reference | CWE-89 (SQL Injection) |
| Severity | Critical (CVSS 9.8) |
| Auto-Fix | ❌ No auto-fix available |
| Category | Security |
Rule Details
Reports three shapes when they reach a raw-SQL sink:
- String concatenation —
sql.raw('SELECT ... ' + value) - Template interpolation —
sql.raw(`SELECT ... ${value}`) - A variable tainted by either, including via
+=, then passed to a sink
Sinks
sql.raw() (on Drizzle's own sql, under any import name) and a plain string
handed to db.execute() / db.run() / db.all() / db.get() /
db.values(). The safe sql tagged template parameterizes its
interpolations.
❌ Incorrect
await sql.raw(`SELECT * FROM users WHERE id = ${userId}`);
await sql.raw('SELECT * FROM users WHERE email = ' + email);
let sql = 'SELECT * FROM products WHERE 1=1';
sql += ` AND name = '${name}'`;
await sql.raw(sql);✅ Correct
db.select()
.from(users)
.where(sql`id = ${userId}`);Known limitations
- A method chosen at runtime —
sql[verb](...)— names nothing to match against the sink list, so it is not reported. The quoted spellingsql['raw'](...)namesrawand IS reported. - Values are followed within the file, and through RELATIVE imports read from
disk. A path alias (
@/lib/db) is not resolved, so a handle imported through one is judged by the call's own shape. - A file that reaches the driver only through a relative module is checked for
the sinks
secure-coding/no-sql-injectiondoes not cover;query()/execute()in such a file belong to that rule, so the line is never reported twice.
What is reported, and what is not (2026-10 review)
A value spliced into the SQL text is reported only when this file cannot prove it fixed:
- Value positions — after
=,<,LIKE, inside'…', in anIN (…)orVALUES (…)list, afterLIMIT— always have a bind parameter, so any value the rule cannot prove fixed is a finding there: a parameter, request data, an unknown call. - Fragment positions — a table or column name, an
ORDER BYtarget, a whole clause or subquery spliced in — have no bind parameter, so composing them is how such statements are built. There a hole is reported once the file traces it to the inbound request (ORDER BY ${req.query.sort}), not merely because it is unproven. - Provably fixed values are never reported: literals and constants, enum
and
const-object members, a ternary between two literals, numbers (Number(),Math.*, arithmetic,.length), a?placeholder list such asids.map(() => '?').join(','), a lookup into a fixed map, a value checked against an anchored-regex or fixed-list allowlist first, a local helper everyreturnof which is fixed, and the driver's own escapers and fragments. - Request data handed over as the whole SQL text is reported too.
The receiver is judged by where it is written, never by its name: a method of
another library that shares the name (/re/.exec, child_process.exec, a
GraphQL client's query) is not a sink, and the statement-shape check
(SELECT … FROM, INSERT INTO, UPDATE … SET, …) applies only when the
receiver cannot be traced to the driver. Test files are skipped.
Implementation
The detection is shared across the driver plugins via createSqlInjectionRule
in @interlace/eslint-devkit; this rule supplies Drizzle's sinks and
remediation copy. Install the plugin matching your stack and you get exactly
one finding per line.
Further Reading
Use this rule
npm install -D oxlint-plugin-drizzle-security{ "jsPlugins": ["oxlint-plugin-drizzle-security"], "rules": { "drizzle-security/no-unsafe-query": "error" }}import { defineConfig } from 'vite-plus';export default defineConfig({ lint: { jsPlugins: ['oxlint-plugin-drizzle-security'], rules: { 'drizzle-security/no-unsafe-query': 'error', }, },});oxlint-plugin-drizzle-security re-exports eslint-plugin-drizzle-security/oxlint: the same rules under the same IDs, and no ESLint install. Why oxlint.
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.
no-raw-identifier-interpolation
Disallow interpolating table, column or sort identifiers into a Drizzle `sql` tagged template, where bind parameters cannot reach them.
no-unscoped-mutation
Require a chained `.where()` on Drizzle delete and update builders, so a bulk mutation cannot rewrite or delete every row in the table.