Skip to main content
ESLint Interlace
Plugin: pgRules

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

AspectDetails
CWE ReferenceCWE-89 (SQL Injection)
SeverityCritical (CVSS: 9.8)
Auto-Fix❌ No auto-fix available
CategorySecurity
ESLint MCPβœ… Optimized for AI assistant integration
Best ForProtecting database operations from SQL injection vulnerabilities

Value & investment case

Why this rule pays for itself. Framework: cicd-impact/philosophy.md.

DimensionValue
CWECWE-89 β€” Improper Neutralization of Special Elements used in an SQL Command (CVSS 9.8 Critical)
Feedback-loop tierEditor / 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 relevanceCritical: fintech (regulatory + transaction data), healthtech (PHI), B2B SaaS (multi-tenant exposure), cybersecurity Β· High: marketplaces, infra/devtools Β· Medium: B2C
Investor-frame impactSQL 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 β€” on client.query(), pool.query(), or any object whose method is named query.
  • Parameterization convention: pg uses $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

ComponentPurposeExample
Risk StandardsSecurity benchmarksCWE-89 OWASP:A05 CVSS:9.8
Issue DescriptionSpecific vulnerabilitySQL Injection detected
Severity & ComplianceImpact assessmentCRITICAL [SOC2,PCI-DSS,HIPAA,ISO27001]
Fix InstructionActionable remediationFollow the remediation steps below
Technical TruthOfficial referenceOWASP 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 verify

Mitigation: 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

If this rule caught a real vulnerability in your codebase, ⭐ star the repo β€” it keeps the detection logic maintained.