no-unsafe-query
Prevent SQL injection by disallowing string concatenation or interpolated template literals in knex.raw() calls.
CWE: CWE-89 OWASP: A03:2021 – Injection
Detects SQL injection in Knex raw queries. This rule is part of eslint-plugin-knex-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 —
knex.raw('SELECT ... ' + value) - Template interpolation —
knex.raw(`SELECT ... ${value}`) - A variable tainted by either, including via
+=, then passed to a sink
Sinks
knex.raw() (also on knex.schema and a transaction) and every builder method
that takes raw SQL: whereRaw, orWhereRaw, andWhereRaw, havingRaw,
orHavingRaw, andHavingRaw, orderByRaw, groupByRaw, joinRaw,
fromRaw, plus objection's standalone raw(). ?? identifier placeholders
and bindings are the fix.
❌ Incorrect
await knex.raw(`SELECT * FROM users WHERE id = ${userId}`);
await knex.raw('SELECT * FROM users WHERE email = ' + email);
let sql = 'SELECT * FROM products WHERE 1=1';
sql += ` AND name = '${name}'`;
await knex.raw(sql);✅ Correct
knex.raw('SELECT * FROM users WHERE id = ?', [userId]);Known limitations
- A method chosen at runtime —
knex[verb](...)— names nothing to match against the sink list, so it is not reported. The quoted spellingknex['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 Knex's sinks and
remediation copy. Install the plugin matching your stack and you get exactly
one finding per line.
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.
no-mass-assignment
Disallow writing an inbound request object straight to the database through knex, which lets the caller set every column the model exposes.
no-unscoped-mutation
Require a chained `.where()` on Knex delete and update builders, so a bulk mutation cannot rewrite or delete every row in the table.