no-unsafe-query
SQL injection is one of the most critical security vulnerabilities
Prevents SQL injection by detecting string concatenation or template literals with variables in
client.query()calls.
π¨ Security rule | π‘ Provides LLM-optimized guidance | β οΈ Set to error in recommended
Quick Summary
| Aspect | Details |
|---|---|
| CWE Reference | CWE-89 (SQL Injection) |
| Severity | Critical (CVSS: 9.8) |
| Auto-Fix | β No auto-fix available |
| Category | Security |
| ESLint MCP | β Optimized for AI assistant integration |
| Best For | Protecting database operations from SQL injection vulnerabilities |
Value & investment case
Why this rule pays for itself. Framework:
cicd-impact/philosophy.md.
| Dimension | Value |
|---|---|
| CWE | CWE-89 β Improper Neutralization of Special Elements used in an SQL Command (CVSS 9.8 Critical) |
| Feedback-loop tier | Editor / pre-commit (sub-second) β cheapest layer per the feedback-loop hierarchy |
| Defensive-layer leverage | ~10Γ cheaper than unit-test Β· ~1,000Γ cheaper than production rollback Β· 10,000+Γ cheaper than disclosure β SQL injection is the most-cited OWASP A03 finding (cost-ratio anchors) |
| Niche relevance | Critical: fintech (regulatory + transaction data), healthtech (PHI), B2B SaaS (multi-tenant exposure), cybersecurity Β· High: marketplaces, infra/devtools Β· Medium: B2C |
| Investor-frame impact | SQL injection β full database disclosure β mandatory disclosure cycle. The most-cited single attack class in security-incident reports for two decades. Lint-time enforcement of parameterized queries is the cheapest possible structural defense. |
Read also: philosophy.md Β§investor-frame Β· niche-presets.json Β· analyzer-evaluation-framework.md
Why no one else does this
Generic SQL injection detectors flag string concatenation wherever they see it β but they have no knowledge of how each database client parameterizes queries. They can tell you "this string was built with +"; they cannot tell you whether it was ever passed to client.query() or whether the driver's parameterization convention was respected.
pg/no-unsafe-query knows the pg (node-postgres) driver's contract specifically:
- API surface: it only fires on
.query()calls β onclient.query(),pool.query(), or any object whose method is namedquery. - Parameterization convention:
pguses$1, $2, β¦positional placeholders with a second-argument array. A call that has a second argument is already parameterized; the rule stays silent. - Variable assignment taint tracking: the rule maintains a taint map across the function scope. If a variable is assigned an unsafe string (
const sql = "SELECT..." + id) and that variable is later passed to.query(sql), the call is flagged β even though the concatenation and the query call are on separate lines.
No generic SQL injection linter has all three of these constraints encoded together. The result is far fewer false positives (parameterized calls never fire) and far fewer false negatives (split-line taint is caught).
What gets caught
Direct string concatenation
// Flagged: concatenation directly inside .query()
const result = await client.query(
"SELECT * FROM users WHERE id = " + userId
);Template literal with interpolation
// Flagged: template literal with a runtime expression inside .query()
const result = await client.query(
`SELECT * FROM users WHERE id = ${userId}`
);Variable assignment taint (split-line)
// Flagged: the variable was tainted by unsafe construction
// before it was passed to .query()
const sql = "SELECT * FROM users WHERE name = '" + userName + "'";
await pool.query(sql);The rule tracks sql through the VariableDeclarator and flags the .query(sql) call even though there is no concatenation on that line.
What doesn't fire (zero FPs)
Positional placeholders β the pg parameterization contract
// Safe: second argument provides the values array
const result = await client.query(
"SELECT * FROM users WHERE id = $1",
[userId]
);Object form with text + values
// Safe: object-form query β text is a static string, values is the array
const result = await pool.query({
text: "SELECT * FROM users WHERE name = $1",
values: [userName],
});Both forms satisfy pg's parameterization contract. Neither fires a lint error.
Rule Details
SQL injection is one of the most critical security vulnerabilities. This rule detects potentially unsafe SQL query construction in pg driver calls.
β Incorrect
// Template literal with variable
const result = await client.query(`SELECT * FROM users WHERE id = ${userId}`);
// String concatenation
const query = "SELECT * FROM users WHERE name = '" + userName + "'";
await pool.query(query);β Correct
// Parameterized query
const result = await client.query('SELECT * FROM users WHERE id = $1', [
userId,
]);
// Named parameters (with pg-named or similar)
const result = await client.query({
text: 'SELECT * FROM users WHERE id = $1',
values: [userId],
});Error Message Format
The rule provides LLM-optimized error messages (Compact 2-line format) with actionable security guidance:
π CWE-89 OWASP:A05 CVSS:9.8 | SQL Injection detected | CRITICAL [SOC2,PCI-DSS,HIPAA,ISO27001]
Fix: Review and apply the recommended fix | https://owasp.org/Top10/A05_2021/Message Components
| Component | Purpose | Example |
|---|---|---|
| Risk Standards | Security benchmarks | CWE-89 OWASP:A05 CVSS:9.8 |
| Issue Description | Specific vulnerability | SQL Injection detected |
| Severity & Compliance | Impact assessment | CRITICAL [SOC2,PCI-DSS,HIPAA,ISO27001] |
| Fix Instruction | Actionable remediation | Follow the remediation steps below |
| Technical Truth | Official reference | OWASP Top 10 |
Known False Negatives
The following patterns are not detected due to static analysis limitations:
Tagged Template Literals (sql...)
Why: Tagged templates like sql from libraries are function calls, not template literals.
// β NOT DETECTED - appears safe but may not be
import { sql } from 'some-library';
await client.query(sql`SELECT * FROM users WHERE id = ${userId}`);
// If 'sql' doesn't properly escape, this is vulnerable!Mitigation: Use a library verified to properly escape tagged templates.
Dynamic Query Variables
Why: When the query is stored in a variable, we can't analyze its construction.
// β NOT DETECTED
const unsafeQuery = buildQuery(userInput); // May concatenate strings internally
await client.query(unsafeQuery);Mitigation: Always use parameterized queries ($1, $2) directly in literals.
Nested Function Calls
Why: Queries passed through helper functions aren't traced.
// β NOT DETECTED
function executeQuery(query: string) {
return client.query(query);
}
executeQuery(`SELECT * FROM users WHERE id = ${userId}`);Mitigation: Apply the rule to helper functions that execute queries.
Format Functions with User Input
Why: The rule doesn't track data flow through pg-format or similar.
// β NOT DETECTED - but format() should handle escaping
import format from 'pg-format';
await client.query(format('SELECT * FROM %I.users', userSchema));
// Safe if format() escapes, but rule can't verifyMitigation: Use parameterized queries for values; use verified formatters only for identifiers.
When Not To Use It
- When using a query builder (Drizzle, Kysely) that handles parameterization
- In migration files with static SQL
Related Rules
- check-query-params - Validates parameter count
- no-batch-insert-loop - Prevents N+1 queries
If this rule caught a real vulnerability in your codebase, β star the repo β it keeps the detection logic maintained.