Skip to main content
interlace
Plugin: sqlite-securityRules

no-unsafe-query

Prevent SQL injection by disallowing string concatenation or interpolated template literals in SQLite statements.

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

Detects SQL injection in SQLite raw queries. This rule is part of eslint-plugin-sqlite-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 — db.prepare('SELECT ... ' + value)
  2. Template interpolation — db.prepare(`SELECT ... ${value}`)
  3. A variable tainted by either, including via +=, then passed to a sink

Sinks

.prepare(), .exec(), .run(), .all(), .get() and sqlite3's .each(), bun:sqlite's .query(), @libsql/client's .execute() (including { sql, args }), and better-sqlite3's .pragma(). The sqlite promise wrapper is covered too. A prepared statement's .get(id) / .run(id) take bound values, not SQL, and are not sinks.

❌ Incorrect

await db.prepare(`SELECT * FROM users WHERE id = ${userId}`);

await db.prepare('SELECT * FROM users WHERE email = ' + email);

let sql = 'SELECT * FROM products WHERE 1=1';
sql += ` AND name = '${name}'`;
await db.prepare(sql);

✅ Correct

db.prepare('SELECT * FROM users WHERE id = ?').get(userId);

Known limitations

  • A method chosen at runtime — db[verb](...) — names nothing to match against the sink list, so it is not reported. The quoted spelling db['prepare'](...) names prepare 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 SQLite'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-sqlite-security
.oxlintrc.json
{  "jsPlugins": ["oxlint-plugin-sqlite-security"],  "rules": {    "sqlite-security/no-unsafe-query": "error"  }}
vite.config.ts
import { defineConfig } from 'vite-plus';export default defineConfig({  lint: {    jsPlugins: ['oxlint-plugin-sqlite-security'],    rules: {      'sqlite-security/no-unsafe-query': 'error',    },  },});

oxlint-plugin-sqlite-security re-exports eslint-plugin-sqlite-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.