Ask a backend engineer how the database layer fails and you'll hear "SQL
injection." It's real, it's CWE-89, and it's one of four structural ways a
node-postgres data layer breaks in production. The other three — identifier
hijacking, connection-pool exhaustion, insecure transport — don't make the OWASP
headlines, but they page you at 3 AM all the same.
Each is a structural pattern (it survives code review precisely because each
line looks fine in isolation), and each has a dedicated rule in
eslint-plugin-pg. Here's the threat model and the rule that closes it.
| # | Failure mode | What an attacker (or load) controls | The pg rule | CWE |
|---|---|---|---|---|
| 1 | Injection via values | a value spliced into the query string | no-unsafe-query |
CWE-89 |
| 2 | Identifier hijacking | a table/schema name (search_path) |
no-unsafe-search-path |
CWE-426 |
| 3 | Connection exhaustion | a leaked pool client → pool empties | no-missing-client-release |
CWE-404 |
| 4 | Insecure transport | TLS turned off to the database | no-insecure-ssl |
CWE-319 |
1. Injection via values — no-unsafe-query (CWE-89)
The classic. A user-controlled value is concatenated or interpolated into the
SQL text instead of being passed as a parameter:
client.query(`SELECT * FROM users WHERE id = ${req.query.id}`); // ❌
client.query("SELECT * FROM users WHERE id = $1", [req.query.id]); // ✅
The $1 placeholder + values array is pg's escaping contract — the driver
handles quoting and types, and the pattern can't be accidentally broken.
src/users.js
3:3 error 🔒 CWE-89 OWASP:A03-Injection CVSS:9.8 | Unsafe SQL query detected. Variable interpolation found. | CRITICAL [SOC2,PCI-DSS,NIST-CSF]
Fix: Use parameterized queries ($1, $2) instead of string concatenation.
(The ESLint CLI also appends the rule's doc URL to the Fix: line; it's trimmed
here for width.) The rule fires on +-concatenation, ${…} template expressions, and
cross-line tainted variables in .query() calls — the full taxonomy is in
Three SQL Injection Patterns That Still Ship.
2. Identifier hijacking — no-unsafe-search-path (CWE-426)
Here's the part parameterization can't help with: $1 binds values, not
identifiers. A table, column, or schema name can't be a bind parameter — so
when the schema is dynamic, teams fall back to interpolation, and an attacker who
controls search_path re-points an unqualified SELECT * FROM users at their
own table.
await client.query(`SET search_path TO ${tenant}`); // ❌ identifier injection
SET rejects parameters, so the fix is identifier-escaping (pg-format's %I)
or an allow-list — not a bind. The full attack and the defenses are in
search_path Hijacking.
3. Connection exhaustion — no-missing-client-release (CWE-404)
Not an attacker — just normal load against a leak. A pool.connect() whose
client is never released drains a 100-connection pool in minutes, and then
every query times out:
const client = await pool.connect();
const rows = await client.query("..."); // ❌ no client.release() — leaked
Release in a finally, or use pool.query() for single-shot queries. The 3 AM
post-mortem is in
The Connection Leak That Exhausted Our Pool.
4. Insecure transport — no-insecure-ssl (CWE-319)
The one-line config that ships secrets in cleartext to the database:
new Pool({ ssl: { rejectUnauthorized: false } }); // ❌ accepts any cert (MITM)
rejectUnauthorized: false disables certificate validation — convenient against
a self-signed dev cert, catastrophic in production. no-insecure-ssl flags it;
use a real CA bundle (ssl: { ca: fs.readFileSync(...) }) instead.
One config turns on all four
# npm
npm install --save-dev eslint-plugin-pg
# yarn
yarn add -D eslint-plugin-pg
# pnpm
pnpm add -D eslint-plugin-pg
# bun
bun add -d eslint-plugin-pg
// eslint.config.mjs — `configs` is a NAMED export (default export is the plugin)
import { configs } from "eslint-plugin-pg";
export default [
// scope to where the database code lives
{
files: ["**/db/**", "**/repositories/**", "**/models/**"],
...configs.recommended,
},
];
# CI — block the PR on any new data-layer finding
- run: npx eslint . --max-warnings 0
Compatibility
| Surface | Support |
|---|---|
| Package managers | npm, yarn, pnpm, bun |
| Node | >= 18.0.0 |
| ESLint | `^8.0.0 \ |
{% raw %}pg driver |
peer `^6 \ |
| Module system | CommonJS — {% raw %}eslint.config.js or .mjs
|
| Oxlint | Loads under Oxlint's JS-plugin runner via the interlace-pg port, parity-gated in CI |
Where this fits
This is the data-layer threat map. Each failure mode has a dedicated deep-dive,
and the full plugin tour covers the rest of the 13 rules:
-
Three SQL Injection Patterns — the
no-unsafe-querydetection in depth - search_path Hijacking — the identifier attack most teams have never heard of
- The Connection Leak Outage — the 3 AM pool-exhaustion post-mortem
-
Getting Started with
eslint-plugin-pg— all 13 rules end to end
Links
⭐ Star on GitHub if your data layer fails any of these four ways.
I'm Ofri Peretz, a security engineering leader and the author of the
Interlace ESLint ecosystem — domain-specific static analysis for security,
reliability, and performance on the Node.js stack. eslint-plugin-pg is its
node-postgres layer.
Top comments (0)