Skip to main content
interlace
Plugin: knex-securityRules

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

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

Rule Details

Reports three shapes when they reach a raw-SQL sink:

  1. String concatenation — knex.raw('SELECT ... ' + value)
  2. Template interpolation — knex.raw(`SELECT ... ${value}`)
  3. 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 spelling knex['raw'](...) names raw and 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-injection does 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 an IN (…) or VALUES (…) list, after LIMIT — 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 BY target, 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 as ids.map(() => '?').join(','), a lookup into a fixed map, a value checked against an anchored-regex or fixed-list allowlist first, a local helper every return of 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.