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
| 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 —
dataSource.query('SELECT ... ' + value) - Template interpolation —
dataSource.query(`SELECT ... ${value}`) - 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 spellingdataSource['query'](...)namesqueryand 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. 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{ "jsPlugins": ["oxlint-plugin-typeorm-security"], "rules": { "typeorm-security/no-unsafe-query": "error" }}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.
no-mass-assignment
Disallow writing an inbound request object straight to the database through TypeORM, which lets the caller set every column the model exposes.
require-tls
Require TLS on TypeORM DataSource connections, so queries and credentials are not sent in cleartext and the server is authenticated.