Contents
In brief
SQL injection in Node.js has not gone away: TypeScript and code review often miss three familiar patterns in node-postgres. A Dev.to article shows how eslint-plugin-pg catches them statically — in the editor, without heavy SAST.
What happened
The story is familiar: linter clean, reviewer approved, release shipped. Three months later a pentester finds pool.query("SELECT * FROM orders WHERE user_id = " + req.query.userId). Everyone knows parameterized queries; OWASP has flagged injection for years — yet vulnerabilities still slip through because each pattern looks harmless line by line.
The author names three forms. Direct concatenation in the first .query() argument. Template literals with ${userId} — cleaner syntax, same risk. Variable assignment: const sql = "…" + category then client.query(sql) — the call site looks fine while the string was poisoned two lines earlier.
The rule pg/no-unsafe-query targets pg, not “any string near SELECT”. It only fires on .query(), understands the $1 + values-array contract, and marks variables tainted at assignment. A sanitize() wrapper does not help — the rule cannot prove it is equivalent to parameterization.
Why it matters
Generic linters either noise on innocent string building or miss template literals. Intraprocedural ESLint does not trace taint across the whole repo, but it runs on every save and in pre-commit — where developers actually edit SQL.
Teams on Prisma, Knex, or Drizzle are not immune: $queryRaw, knex.raw, and similar escape hatches bring back the same three patterns. For services on plain pg — internal APIs, pipelines, microservices — that is often the real attack surface.
In practice
- Add eslint-plugin-pg and enable
pg/no-unsafe-queryas error. - Where user input hits SQL, use
$1,$2, and a values array — not concatenation or bare${}without placeholders. - Do not “refactor” a parameterized query into a “readable” string — a common regression path.
- For schema/table identifiers use pg-format or a dedicated layer, not
+ SCHEMA_NAME(a known false-positive case). - Keep Semgrep/CodeQL for interprocedural taint; use ESLint for instant IDE feedback.
Takeaway
SQL injection in Node.js is not a “junior mistake” problem — it is a pattern problem that fools review. A pg-specific ESLint rule closes three production shapes; worth reading if query("…" + id) still lives in your tree.