DEV Community

Cover image for SonarJS Has 269 Rules. On 40 Vulnerabilities It Caught 14 — It Misses 65% of the Security Surface.
Ofri Peretz
Ofri Peretz

Posted on • Edited on • Originally published at ofriperetz.dev

SonarJS Has 269 Rules. On 40 Vulnerabilities It Caught 14 — It Misses 65% of the Security Surface.

eslint-plugin-sonarjs ships 269 rules and clears 3M+ weekly downloads.
I ran it against a fixture of 40 known Node.js vulnerabilities. It caught
14. The Interlace security plugins caught 40 of 40 on the same file.

Against that 40-vuln baseline, SonarJS's recall is 35% — it misses 65%
of the security surface, and the 26 it misses are not edge cases. They are fs
path traversal, prototype pollution, PostgreSQL injection, unsafe
deserialization, DOM XSS, SSRF, timing attacks. The Node.js attack surface, the
part that actually ships to production.

If your security gate is "SonarJS is green," that 65% is the gap you're
shipping.

That's not a knock on SonarJS — it's a scope result. SonarJS is one of
the best quality linters in the ecosystem that happens to carry a handful of
cross-language security rules. Node-specific security depth is a different
product. Here's the reproducible data, why each gap survives review, and why
"run both" is the only honest answer.

Every number below is from two committed result files — SonarJS:
results/fn-fp-comparison/2026-05-29.json,
Interlace:
2026-05-30.json.
Each plugin runs in isolation against the identical fixture under its
recommended preset; TP/FP/FN are computed by mapping violations to the 40
vulnerable and 38 safe functions. Versions and the exact commands are at the
bottom — run it on your own repo for an unbiased read.

Detection — vulnerable.js (40 vulnerabilities, 14 categories)

Config TP (of 40) Recall Misses
eslint-plugin-sonarjs (recommended) 14 35.0% 26
Interlace security plugins (recommended) 40 100% 0

SonarJS's 14 true positives came from exactly the rules it ships for
cross-language security basics. Straight from the run's byRule block:
sonarjs/os-command (×4), sonarjs/code-eval (×2), sonarjs/hashing (×2),
sonarjs/pseudo-random (×2), sonarjs/no-hardcoded-passwords (×1),
sonarjs/hardcoded-secret-signatures (×2), sonarjs/insecure-jwt-token (×1),
sonarjs/slow-regex (×1). Those are genuinely good rules — os-command in
particular is best-in-class.

The 26 SonarJS misses are the Node-specific depth it has no rule for. The
Interlace run catches every one; a sample of which rule fires, from that run's
byRule block: fs-path traversal
(node-security/detect-non-literal-fs-filename ×4), object injection /
prototype-pollution (secure-coding/detect-object-injection ×4), PostgreSQL
injection (pg/no-unsafe-query ×4), unsafe deserialization
(secure-coding/no-unsafe-deserialization ×3), DOM XSS
(browser-security/no-innerhtml ×1), SSRF (node-security/no-ssrf ×2),
NoSQL injection (mongodb-security/no-unsafe-query ×2), timing attacks
(secure-coding/no-insecure-comparison ×2), and more.

If those rule IDs map to lines in your own codebase, you don't need to read to
the end first. Add the security half next to SonarJS now — it's additive, not a
replacement:

npm i -D eslint-plugin-secure-coding eslint-plugin-node-security \
  eslint-plugin-pg eslint-plugin-browser-security
Enter fullscreen mode Exit fullscreen mode

The full flat-config block — SonarJS for quality, these four for security in the
same run — is below.

False positives — safe-patterns.js (38 safe functions)

Config Security false positives
Interlace security plugins 0 (FPR 0.0%)
eslint-plugin-sonarjs (recommended) 5 (FPR 13.2%)

This one cut against my prior. SonarJS is usually held up as the precise tool,
but on the safe fixture it raised 5 security false positives — from the
run's safe-file byRule: sonarjs/no-os-command-from-path (×3) on validated /
no-shell spawn calls, plus sonarjs/pseudo-random (×1) on a non-crypto shuffle
and sonarjs/slow-regex (×1) on a simple regex. They're defensible heuristics,
not bugs — but they are noise on safe code, so "SonarJS is the precise one" is
not what the numbers say here. The Interlace security run flagged 0 of the
38 safe functions (precision 100% on this fixture).

Where SonarJS is the right call

  • Cognitive complexity — one of the best implementations anywhere.
  • Dead code / unused assignments / redundant logic — the no-dead-store, no-unused-collection, all-identical-comparison family.
  • Code smells — duplicate branches, collapsible conditionals.
  • The security basics — command injection, eval, weak hashing, insecure randomness, hardcoded-secret signatures, the slow-regex pattern. These are the 14 it caught.

Where you need domain security rules

SonarJS has no rule for most of the Node-specific attack surface: SQL string
concatenation, fs path traversal, prototype pollution, NoSQL/Mongo injection,
SSRF, open redirect, timing attacks, JWT claim validation (no-algorithm /
no-expiry), unsafe deserialization. Every one of those is in the run's
missedVulnerabilities list. Those are the gaps the domain plugins fill — not
because SonarJS is weak, but because it was built for the breadth of
JavaScript quality
, not the depth of Node.js security.

Why these 26 survive code review

None of the 26 SonarJS-misses are exotic. They survive review because of a
process gap I've watched play out on real teams, not a knowledge gap.

A senior reviewer opens the PR. CI is green — SonarJS, the linter with 269
rules, found nothing. The diff is a fs.readFile(path.join(base, req.query.f))
or a pool.query('SELECT * FROM users WHERE id = ' + id). The reviewer's brain
runs the same shortcut everyone's does under a full sprint: "the security
linter would have caught it if it mattered."
Green tool, trusted name, big
rule count — approve.

That's the trap. SonarJS being excellent at what it does is exactly what
makes its silence on fs traversal read as a clean bill of health. The reviewer
isn't being careless — they're trusting a tool whose scope they never checked.
A 269-rule linter that finds zero security issues feels like proof of safety. It
isn't. It's proof you ran a quality linter.

The fix isn't "review harder." It's making the gate's scope honest: a
Node-security rule that fails the build on the exact line, with the CWE attached,
so "CI is green" actually means the Node attack surface was checked.

What happens when AI writes the code

This stopped being only a human-reviewer problem the day a model started writing
the diff. AI assistants reproduce the Node-specific classes SonarJS has no rule
for, because the unsafe form is the most common form in their training data:
fs/path.join straight onto request input, pg queries built by string
concatenation, prototype-pollution sinks, unsafe deserialization. (SonarJS does
catch the cross-language basics it ships rules for — os-command, weak hashing,
eval — which is exactly why a green run lulls everyone past the gaps it can't
see. Note it did not catch the SQL-injection class on this fixture: all four
vuln_sql_* cases are in its missedVulnerabilities list.) I've measured the
model side of this directly —
65-75% of Claude-generated Node functions shipped with a
vulnerability
,
and the same SonarJS-blind classes recurred when Claude wrote a full NestJS
service
.

It is not a Claude problem — Gemini reproduces the same blind spots

To be sure this wasn't one vendor, I ran the same generate-then-scan harness on
Gemini. Asked to write 195 ordinary Node.js functions (database access,
auth, file I/O, command execution, secrets) at 10 samples per prompt,
gemini-3-pro-cli shipped a vulnerability in 113 of them — a 58%
vulnerability rate, average CVSS 8.9

(checkpoint JSON).
The rules that fired on Gemini's code are the exact families SonarJS has no rule
for: pg/no-unsafe-query, secure-coding/detect-object-injection,
node-security/detect-non-literal-fs-filename, node-security/no-ssrf,
jwt/require-expiration, jwt/require-algorithm-whitelist. Same prompt class,
same blind spot, different model. (I split the model-vs-model detail out into a
Claude-vs-Gemini NestJS
comparison

if you want the per-error breakdown.)

So the loop tightens: the model — Claude or Gemini — emits a pg injection,
TypeScript compiles, SonarJS stays green, the human applies the "the linter
would've caught it" shortcut, and it ships. Every layer is individually
reasonable; the surface nobody owns is Node-specific security. That's the layer
the domain plugins are built for — and it's the layer that has to hold when the
author isn't a human who can be asked "did you sanitize this?"

The real answer: run both

SonarJS for quality, the domain plugins for security. They don't overlap enough
to conflict, and together they cover both axes:

// eslint.config.mjs
import sonarjs from "eslint-plugin-sonarjs";
import { configs as secureCoding } from "eslint-plugin-secure-coding";
import { configs as nodeSecurity } from "eslint-plugin-node-security";
import { configs as pg } from "eslint-plugin-pg";
import { configs as jwt } from "eslint-plugin-jwt";
import { configs as mongodbSecurity } from "eslint-plugin-mongodb-security";
import { configs as browserSecurity } from "eslint-plugin-browser-security";

export default [
  sonarjs.configs.recommended, // quality
  secureCoding.recommended, // general security, proto-pollution, deserialization
  nodeSecurity.recommended, // crypto, SSRF, fs, child_process
  pg.recommended, // PostgreSQL
  jwt.recommended, // JWT claim/algorithm validation
  mongodbSecurity.recommended, // NoSQL injection
  browserSecurity.recommended, // DOM / browser
];
Enter fullscreen mode Exit fullscreen mode

Methodology — reproduce it

Honest disclosure: the fixtures are team-authored (vulnerable.js, 40
vulnerabilities across 14 categories; safe-patterns.js, 38 validated-safe
functions), so they measure coverage of the surface the Interlace rules target —
run it on your own code for an unbiased read.

The numbers are not asserted; they're in two committed result files you can
diff line-by-line:
SonarJS run
and
Interlace run.
Versions are pinned from each file's installedVersions block:
eslint-plugin-sonarjs@3.0.6 (269 rules) for the SonarJS run;
eslint-plugin-secure-coding@3.0.2, eslint-plugin-node-security@4.2.0,
eslint-plugin-pg@1.4.3, eslint-plugin-jwt@2.2.3,
eslint-plugin-mongodb-security@8.2.3, eslint-plugin-browser-security@1.2.3
for the Interlace run. Each plugin runs in isolation under its recommended
preset, --format json, and violations are mapped to the 40 vulnerable / 38
safe functions to compute TP / FP / FN.

npm i -D eslint-plugin-sonarjs@3.0.6 eslint-plugin-secure-coding@3.0.2 \
  eslint-plugin-node-security@4.2.0 eslint-plugin-pg@1.4.3 \
  eslint-plugin-jwt@2.2.3 eslint-plugin-mongodb-security@8.2.3 \
  eslint-plugin-browser-security@1.2.3
npx eslint --config eslint.config.sonarjs.mjs test-files/vulnerable.js --format json
npx eslint --config eslint.config.interlace.mjs test-files/vulnerable.js --format json
Enter fullscreen mode Exit fullscreen mode

(On ESLint 8, set ESLINT_USE_FLAT_CONFIG=true to load eslint.config.mjs;
ESLint 9+ uses flat config by default. For the Oxlint and eslint-plugin-security
rows on the same fixtures, see the
4-way benchmark
in the comprehensive 17-plugin run, eslint-plugin-security scored 0 true
positives on this fixture.)

More in the ESLint Security Benchmark Series

This is the SonarJS cut of a larger comparison set — same fixtures, same
recommended-preset methodology, one tool per article:


Compatibility

Surface Support
Package managers npm, yarn, pnpm, bun
Node >= 18.0.0
ESLint `^8.0.0 \
Module system Plugins ship CommonJS; your config can be {% raw %}eslint.config.js or .mjs
Oxlint Interlace flagship rules run via the interlace-* ports, parity-gated

Your turn

Pull the last fs, child_process, or pool.query bug that made it to
production in your codebase. Go back to that PR. Was the security linter green
when it was approved? I'd bet it was — and I want to hear the one that got
through. What was the line, and what tool said it was fine? Drop it in the
comments.


Links

⭐ Star on GitHub if you run SonarJS for quality and want the Node.js security half — the layer that has to hold when AI is writing the diff — to match.


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.

ofriperetz.dev · LinkedIn · GitHub

Top comments (0)