Skip to main content
interlace
Plugin: typeorm-securityRules

no-unsafe-query

Prevent SQL injection by disallowing string concatenation or interpolated template literals in TypeORM raw queries.

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

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

Sinks

dataSource.query() / manager.query() / repository.query(), the QueryBuilder clauses that take a raw SQL fragment (where, andWhere, orWhere, having and its variants, orderBy, addOrderBy, groupBy, addGroupBy, select, addSelect, and the condition argument of the join methods), and the Raw() find operator — every hole but the alias TypeORM substitutes. Use :name parameters.

❌ Incorrect

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

await dataSource.query('SELECT * FROM users WHERE email = ' + email);

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

✅ Correct

await dataSource.query('SELECT * FROM users WHERE id = $1', [userId]);

Known limitations

  • A method chosen at runtime — dataSource[verb](...) — names nothing to match against the sink list, so it is not reported. The quoted spelling dataSource['query'](...) names query 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. An escaper's output carries its own quotes, so inside '…' or an identifier quote it closes the author's token: there an escaped value is reported unless what was escaped is itself fixed (Quint oracle, 2026-10).
  • 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 TypeORM'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-typeorm-security
.oxlintrc.json
{  "jsPlugins": ["oxlint-plugin-typeorm-security"],  "rules": {    "typeorm-security/no-unsafe-query": "error"  }}
vite.config.ts
import { defineConfig } from 'vite-plus';export default defineConfig({  lint: {    jsPlugins: ['oxlint-plugin-typeorm-security'],    rules: {      'typeorm-security/no-unsafe-query': 'error',    },  },});

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