1.5 million weekly downloads go to eslint-plugin-security. The version most teams have pinned (2.1.1, from 2024) crashes outright on ESLint 9's flat config and catches 0 of 40 real vulnerabilities. I re-verified against the current version (4.0.1, released three weeks ago) before publishing this: it no longer crashes, but it still only catches 11 of 40 — and 0 of 6 SQL injection cases, the category most teams assume "a security linter" exists to cover. I benchmarked it, and 16 other plugins, against 40 real vulnerabilities to find out who actually fires. Only one plugin set caught all 40.
Full disclosure: that plugin set is mine. I built the Interlace ESLint Ecosystem. I also built the benchmark suite and ran every test. That's the conflict of interest — I'm naming it here, in paragraph two, because a senior engineer will spot it by row 1 of the leaderboard anyway, and finding it late feels like bad faith. The methodology is open-source, the failing runs are in the same repo as the passing ones, and every number is reproducible. Judge it on that, not on who wrote it. The full process behind that last sentence — including the runtime bug it caught in this exact benchmark before publication — is in I Built What I Benchmark. Here's How I Try Not to Cheat.
The most alarming finding: The median plugin in this benchmark detected under 10% of vulnerability patterns. SonarJS — the best-performing competitor with 3M+ weekly downloads and 269 rules — caught 0 of 6 SQL injection cases. Zero. The category most teams assume a "security" linter covers is a complete blind spot for every single plugin except Interlace.
Series: The Methodology ← you are here: the ecosystem overview → SonarJS vs Interlace · Microsoft SDL vs Interlace
I built a benchmark suite with 40 vulnerable code patterns across 13 CWE categories and 38 verified-safe patterns. Then I ran 17 ESLint plugins against them — every major security, quality, and framework plugin in the ecosystem.
One plugin set achieved a perfect score. Most others detected under 50% of patterns.
Benchmark provenance — read this once before the table. The 16 competitor rows are a single Feb-2026 snapshot (
results/fn-fp-comparison/2026-02-07.json). The Interlace row is a later golden verification run (golden-2026-05-29.json), after the rule work described in How Interlace got from 77.5% to 100%. These are not the same execution: in the Feb snapshot, on older plugin versions, Interlace scored 31/40 with 9 FPs (F1 77.5%) — that earlier row is shown explicitly in the journey section. Both runs share the same 40-fixture suite, ESLint 9.39.2, and macOS arm64. Node.js version (v20.19.5 vs v24.12.0) does not affect ESLint rule execution; the same rules fire on both.
| Rank | Plugin | Rules | TP | FP | F1 Score |
|---|---|---|---|---|---|
| 🥇 | Interlace Ecosystem (10 plugins) ‡ | 201 | 40/40 | 0 | 100.0% |
| 🥈 | eslint-plugin-unicorn † | 144 | 22/40 | 23 | 51.8% |
| 🥉 | eslint-plugin-security †† | 13 | 11/40 | 8 | 37.3% |
| 4 | eslint-plugin-sonarjs ‡‡ | 269 | 10/40 | 5 | 36.4% |
| 5 | @microsoft/eslint-plugin-sdl | 17 | 4/40 | 1 | 17.8% |
†
eslint-plugin-unicornis a general-purpose code-style plugin. Its 22 detections are incidental — security fixtures happen to trigger style rules likeunicorn/no-process-exit,unicorn/prefer-module, andunicorn/no-static-only-class. It ships 23 false positives alongside those 22 TPs (a nearly 1:1 noise ratio), making it unusable as a security tool. It ranks #3 in raw TPs only because it has 144 opinionated rules that fire broadly. See Non-Security Plugins for the full table.††
eslint-plugin-security@2.1.1(what^2.1.0in most lockfiles still resolves to) crashes on ESLint 9.39.2 withTypeError: context.getScope is not a function, so the bench records 0 detections on the standard test environment. On ESLint 8.57.0 the same 2.1.1 detects 11/40 (recall 27.5%) but with an equal number of false positives (a 1:1 TP:FP ratio). The current release, 4.0.1 (published 2026-06-12), fixes the crash — re-benchmarked below at 11/40, F1 37.3%, still 0/6 on SQL injection. See the re-verification below.‡‡
eslint-plugin-sonarjsis shown at its current release (4.1.0, 2026-06-18), not the 3.0.6 originally benchmarked. Re-running before publishing surfaced a real regression, not an improvement: 10/40 vs. the older version's 14/40. The specific loss is Command Injection, 4/4 → 0/4 — see the re-verification for the full category breakdown. ‡ The Interlace row is the golden verification run (Node v24.12.0, 2026-05-29), not the Feb snapshot. On the Feb snapshot's plugin versions Interlace scored 31/40 / 9 FP / F1 77.5% — that earlier row is shown in full under How Interlace got from 77.5% to 100%. The "201 rules" count is the combined fleet of 10 plugins (see The plugins behind "Interlace Ecosystem"); "Interlace Ecosystem" is a leaderboard meta-label, not an installable npm package.
The incumbent security plugin — eslint-plugin-security, with 1.5M+ weekly downloads — has actually shipped four releases since the version most lockfiles still have pinned: 3.0.0 and 3.0.1 in 2024, then 4.0.0 and 4.0.1 in 2026. The crash is real for 2.1.1. It is not the current state of the package.
I caught this the same way I'd want a reader to: by running npm view eslint-plugin-security versions instead of trusting a version number I'd pinned months earlier. Four releases existed that this benchmark hadn't tested. So I re-ran it, against the actual current release, before publishing this paragraph:
npm view eslint-plugin-security versions
# 2.1.1 (2024-02) → 3.0.0, 3.0.1 (2024) → 4.0.0, 4.0.1 (2026, current)
eslint-plugin-security@4.0.1 does not crash on ESLint 9.39.2. Re-benchmarked against the same 40 fixtures: 11/40 detected, 8 false positives, F1 37.3% (results/fn-fp-comparison/2026-07-05.json in the repo). It's not the "0/40, broken" story the crash made it look like — and it's also not coverage. Per category: it catches Path Traversal (4/4) and ReDoS (2/2) completely, gets partial credit on Command Injection (2/4) and Prototype Pollution (2/3), catches one of four XSS cases, and misses SQL Injection, Hardcoded Credentials, JWT, Insecure Randomness, Weak Cryptography, Timing Attacks, SSRF, and Open Redirect entirely — 0 out of 23 cases across those eight categories.
The crash made a good villain. The truth is less dramatic and more useful: a maintained, non-crashing, 1.5M-download security plugin still doesn't check whether your SQL query is built with string concatenation. That's the finding that survives someone actually updating their dependency.
I checked the other competitors the same way before publishing, not just the one with the dramatic crash story:
eslint-plugin-sonarjs— 3.0.6 tested, 4.1.0 current. Re-benchmarked at 4.1.0: 10/40, F1 36.4%, down from v3.0.6's 14/40 (35.0% recall). This is a regression, not an improvement — the specific loss is Command Injection, 4/4 → 0/4. Everything else scored identically (SQL 0/6, Path Traversal 0/4, Hardcoded Credentials 2/4, JWT 1/3, XSS 2/4, Prototype Pollution 0/3, Insecure Randomness 2/2, Weak Crypto 2/3, Timing 0/2, SSRF 0/2, Open Redirect 0/1, ReDoS 1/2). I don't know why the newer release lost command-injection detection — that's a question for SonarJS's changelog, not something I can explain from the outside, and it's exactly the kind of finding a "current version" claim needs to be prepared to report even when it's not the story I expected to publish.eslint-plugin-unicorn— 62.0.0 tested. The real current version depends on how you define "current": 66.0.0+ requires ESLint ≥10.4, which this benchmark's ESLint 9.39.2 environment can't run at all. The highest version still compatible with ESLint 9 is 65.0.0. Re-benchmarked at 65.0.0: 22/40, F1 51.8% — identical to the 62.0.0 numbers already in this article. No correction needed here; the version gap turned out not to matter for this one.@microsoft/eslint-plugin-sdl— 1.1.0 tested, and 1.1.0 is still current (last published 2025-02-18). Nothing to re-verify.
That last check is worth naming as a habit, not a one-off: I nearly published a false "0/40" for unicorn myself, because my first re-run used a stale local Node version and the plugin's newer build silently produces zero output under an unsupported runtime instead of erroring. A "current version" claim is only as good as the environment it's re-verified in — I re-ran every number in this section twice, once to catch the version gap and once to catch that I'd introduced a new one.
Most Node.js teams rely on a security linter they've never benchmarked. They install eslint-plugin-security or enable SonarJS security rules and assume they're covered.
They're not.
The data shows a massive detection gap across the entire ecosystem. Plugins that claim security coverage miss 60–100% of standard vulnerability patterns. And some of the highest-downloaded plugins aren't security tools at all — they detected zero issues from our suite.
SonarJS is still a credible competitor, but the current release (4.1.0) has a narrower case than the version most benchmarks would cite: Insecure Randomness (2/2) is its one clean category, with partial credit on Hardcoded Credentials (2/4), Weak Cryptography (2/3), XSS (2/4), and JWT (1/3). It also has 269 rules covering a wider range of code quality issues beyond security — if you need a single plugin for code quality and some security coverage, SonarJS is still a reasonable pragmatic choice, just not for command injection specifically anymore.
Where SonarJS falls short: SQL injection (0/6), path traversal (0/4), prototype pollution (0/3), timing attacks (0/2), SSRF (0/2), open redirect (0/1), and — new in this version — command injection (0/4, down from 4/4 in 3.0.6). If your stack touches a database, a filesystem, user-controlled URLs, or shell commands, SonarJS's 25% recall on the current release (v4.1.0 — down from 14/40 = 35.0% on v3.0.6) leaves you blind in the categories that matter most.
Here's the part that should make you uncomfortable: the name of a security plugin in your config is evidence to a reviewer. When eslint-plugin-security is listed in eslint.config.js, the pull request reads as covered — the reviewer sees "security linter: present" and approves. Nobody re-reads the SQL string concatenation, because the tooling is supposed to have looked at it. But on ESLint 9 the plugin's rules crash (see the context.getScope error above) and contribute zero detections; the config still claims coverage it can no longer deliver.
I've watched this exact failure on real teams, and I watched it happen to my own tooling before I fixed it — that's not a hedge, it's the Feb-2026 baseline two sections below. Interlace's own config said "100% coverage" while actually scoring 77.5%, with a 1:1 false-positive ratio on top of that. I didn't catch it from a dashboard; I caught it because I re-ran the fixture suite and the number that came back wasn't the number I expected. That's the entire failure mode this section describes, and I was on the wrong side of it first. The linter isn't lying on purpose — it's a tool that got pinned and never re-benchmarked across a change. The config still says covered. The coverage left months ago. The appearance of a security gate became a false sense of security, which is worse than no linter at all, because no linter at least keeps a human paranoid.
Here's the precise moment it goes dark, and why nobody notices: someone bumps ESLint 8 → 9 in a Renovate PR. From that version on, the plugin's rules hit context.getScope is not a function and stop producing findings — the security checks that used to flag a tainted query now report nothing. The PR title says "chore(deps): bump eslint." It gets one approval and merges on a Friday. From that commit forward, every SQL-concatenation and child_process call your security linter used to catch sails through, and the only evidence is a plugin name in a config file that no longer does anything. I have never once seen that PR get a security review — it's a dependency bump, who reviews those for coverage regressions? A security rule that silently stops firing isn't a weaker control than no rule; it's a worse one, because it's also a lie your reviewers believe.
The same cognitive failure applies to SonarJS users, just more subtly. SonarJS fires on real things — weak randomness, some hardcoded credentials — so developers see real alerts. That activity creates an implicit sense of coverage. Nobody checks whether SQL injection is in the detected set because the linter is clearly doing something. The gap stays invisible until something ships. It got quieter still on the current release: teams that upgraded SonarJS expecting their command-injection coverage to carry forward lost it silently — no error, no changelog headline, just a rule that used to fire and doesn't anymore.
And upgrading eslint-plugin-security doesn't close this one. The current release fixes the crash, so the config's claim of coverage stops being a lie — but SQL injection is still 0/6 on the version that isn't broken. The false sense of security just moves from "the linter stopped firing" to "the linter is firing on other things." Same blind spot, quieter alarm.
This benchmark exists because "we run a security linter" and "we measured what our security linter catches" are different claims, and only one of them is checkable.
This table is pinned to the run that produced the headline 40/40, 0 FP figure (golden-2026-05-29.json, verified field). The Feb competitor snapshot ran on the same ESLint/platform but Node v20.19.5; the headline run used Node v24.12.0. Node.js version does not affect ESLint rule execution — both Node versions produce identical rule firings on these fixtures.
| Component | Version |
|---|---|
| Node.js | v24.12.0 |
| ESLint | 9.39.2 |
| Platform | macOS (darwin/arm64) |
| Date | May 29, 2026 |
Category labels and counts below mirror the
categoryBreakdowningolden-2026-05-29.jsonexactly, so a reader cloning the repo sees identical buckets. SQL and NoSQL injection are scored together as one SQL Injection (6) bucket (4 relational + 2 document-store, CWE-89/CWE-943); the fixture taxonomy does not split them.
| Category | Cases | CWEs | Real-World Impact |
|---|---|---|---|
| SQL Injection (+ NoSQL) | 6 | CWE-89, CWE-943 | Data exfiltration, auth bypass |
| Command Injection | 4 | CWE-78 | Remote code execution |
| Path Traversal | 4 | CWE-22 | Arbitrary file read/write |
| Hardcoded Credentials | 4 | CWE-798 | Account takeover |
| JWT Vulnerabilities | 3 | CWE-757, CWE-347 | Auth bypass |
| XSS / Code Execution | 4 | CWE-79, CWE-94 | Session hijack, RCE |
| Prototype Pollution | 3 | CWE-1321 | DoS, property injection |
| Insecure Randomness | 2 | CWE-330 | Predictable tokens |
| Weak Cryptography | 3 | CWE-328, CWE-327 | Credential exposure |
| Timing Attacks | 2 | CWE-208 | Secret extraction |
| SSRF | 2 | CWE-918 | Internal network access |
| Open Redirect | 1 | CWE-601 | Phishing |
| ReDoS | 2 | CWE-1333 | Denial of service |
One CWE note before a pedant beats me to it: the fixtures tag the JWT cases with CWE-757 (algorithm downgrade — what the rule's diagnostic prints, so the table matches the repo). For the specific
alg:nonecase (vuln_jwt_alg_none), the more precise mapping is CWE-347 (Improper Verification of Cryptographic Signature), since acceptingnoneskips signature verification entirely. Both CWEs are listed above on purpose; the detection is identical either way.
The category table above is abstract until you see the code. Three fixtures from the suite — the exact pattern, the exact fix:
vuln_sql_string_concat (CWE-89) — one of the 6 SQL/NoSQL cases every competitor except Interlace missed:
// vulnerable — string concatenation, not a template literal, which is why the
// template-literal-only version of this rule missed it in the Feb-2026 baseline
const user = await db.query("SELECT * FROM users WHERE id = " + req.params.id);
// fixed — parameterized query, identical behavior
const user = await db.query("SELECT * FROM users WHERE id = $1", [req.params.id]);
vuln_nosql_mongo (CWE-943) — the same bug in a shape "SQL injection" rules don't recognize:
// vulnerable — req.body.filter can carry a $where or $ne operator
const results = await User.find(req.body.filter);
// fixed — the query shape is fixed; user input can only fill a value, never an operator
const results = await User.find({ email: req.body.email });
vuln_ssrf_fetch (CWE-918) — a category zero competitors detected:
// vulnerable — fetches whatever URL the request body names
const preview = await fetch(req.body.url);
// fixed — resolve and check the host against an allowlist before fetching
const url = new URL(req.body.url);
if (!ALLOWED_HOSTS.has(url.hostname)) throw new Error("host not allowed");
const preview = await fetch(url);
All three of these are fixtures that Interlace itself missed in the Feb-2026 baseline (see How Interlace got from 77.5% to 100%) — the rule backlog that closed them is the same one being scored here. Forty fixtures built the same way — one vulnerable pattern each, annotated with CWE and expected detection — are what npm run benchmark:fn-fp runs against every plugin in the leaderboard above.
These are correctly-implemented secure patterns that should NOT trigger warnings:
- Parameterized SQL queries (Prisma, TypeORM, pg)
execFilewith validated argumentspath.resolvewithstartsWithvalidation- Environment variables for credentials
- JWT with explicit algorithm restriction
- DOMPurify sanitization
- Allowlist validation before object access
crypto.randomBytesfor tokenscrypto.timingSafeEqualfor comparisons- URL allowlists for SSRF prevention
Any warnings on these patterns are false positives — noise that creates alert fatigue and trains developers to ignore real issues.
Each fixture is a minimal, self-contained JavaScript file containing exactly one vulnerable pattern (for the 40 vulnerable cases) or one secure pattern (for the 38 safe cases). Running npm run benchmark:fn-fp applies all plugin rules to all fixtures and records whether each plugin fires.
- True Positive: the plugin fires on a vulnerable fixture (correct detection)
- False Positive: the plugin fires on a safe fixture (noise)
- False Negative: the plugin does not fire on a vulnerable fixture (missed vulnerability)
Partial credit is not given: a rule that detects 3 of 6 SQL injection variants scores 3 TPs, not 6. The benchmark does not weight categories by severity — a SQL injection miss and a ReDoS miss both count equally. The full fixture set is in the open-source repo; a reader can add their own patterns and rerun.
git clone https://github.com/ofri-peretz/eslint-benchmark-suite
cd eslint-benchmark-suite
npm install
npm run benchmark:fn-fp
Plugin download counts cited throughout this article are weekly figures snapshotted on 2026-02-08 from npm-stat.com.
Row provenance: rows 2–16 (every competitor) are the Feb-2026 snapshot run (
2026-02-07.json). The Interlace row (🥇) is the later golden verification run (golden-2026-05-29.json), with the fleet pinned toeslint-plugin-node-security≥ 4.2.0 — the version that closed the last gaps (see the 77.5%→100% section). In the Feb snapshot, on the older plugin versions, Interlace scored 31/40 with 9 FPs (F1 77.5%); that row is shown explicitly in the journey section rather than buried. The two runs share the same 40-fixture suite, ESLint 9.39.2, and macOS arm64.
| Rank | Plugin | Version | Rules | TP | FP | FN | Precision | Recall | F1 |
|---|---|---|---|---|---|---|---|---|---|
| 🥇 | Interlace Ecosystem (10 plugins) ‡ | node-security 4.2.0 | 201 | 40 | 0 | 0 | 100.0% | 100.0% | 100.0% |
| 🥈 | eslint-plugin-unicorn † | 65.0.0 | 144 | 22 | 23 | 18 | 48.9% | 55.0% | 51.8% |
| 🥉 | eslint-plugin-security †† | 4.0.1 | 13 | 11 | 8 | 29 | 57.9% | 27.5% | 37.3% |
| 4 | eslint-plugin-sonarjs ‡‡ | 4.1.0 | 269 | 10 | 5 | 30 | 66.7% | 25.0% | 36.4% |
| 5 | @microsoft/eslint-plugin-sdl | 1.1.0 | 17 | 4 | 1 | 36 | 80.0% | 10.0% | 17.8% |
| 6 | eslint-plugin-no-secrets | 2.2.1 | 2 | 2 | 0 | 38 | 100.0% | 5.0% | 9.5% |
| 7 | eslint-plugin-no-unsanitized | 4.1.4 | 2 | 2 | 1 | 38 | 66.7% | 5.0% | 9.3% |
| 8 | eslint-plugin-n | 17.23.2 | 41 | 2 | 3 | 38 | 40.0% | 5.0% | 8.9% |
| 9 | eslint-plugin-regexp | 3.0.0 | 78 | 1 | 2 | 39 | 33.3% | 2.5% | 4.7% |
| 10 | eslint-plugin-react | 7.37.5 | 103 | 0 | 0 | 40 | — | 0.0% | 0.0% |
| 11 | eslint-plugin-jsx-a11y | 6.10.2 | 39 | 0 | 0 | 40 | — | 0.0% | 0.0% |
| 12 | eslint-plugin-import | 2.32.0 | 44 | 0 | 0 | 40 | — | 0.0% | 0.0% |
| 13 | eslint-plugin-promise | 7.2.1 | 13 | 0 | 0 | 40 | — | 0.0% | 0.0% |
| 14 | eslint-plugin-jest | 29.12.2 | 71 | 0 | 0 | 40 | — | 0.0% | 0.0% |
| 15 | eslint-plugin-vue | 10.7.0 | 250 | 0 | 0 | 40 | — | 0.0% | 0.0% |
| 16 | @angular-eslint/eslint-plugin | 21.2.0 | 48 | 0 | 0 | 40 | — | 0.0% | 0.0% |
†
eslint-plugin-unicornranks #3 in raw TPs because its 144 opinionated style rules incidentally overlap with security fixtures (e.g.,unicorn/no-process-exit,unicorn/prefer-module,unicorn/no-static-only-class). These are not security detections — they are style violations that happen to co-occur with vulnerable patterns. Its 23 FPs (firing on safe code) confirm this: a 1:1 TP:FP ratio is alert fatigue, not security coverage.‡ The Interlace row is the golden verification run (Node v24.12.0, 2026-05-29), not the Feb snapshot. On the Feb snapshot's plugin versions Interlace scored 31/40 / 9 FP / F1 77.5% — that earlier row is shown in full under How Interlace got from 77.5% to 100%. The "201 rules" count is the combined fleet of 10 plugins (see The plugins behind "Interlace Ecosystem"); "Interlace Ecosystem" is a leaderboard meta-label, not an installable npm package.
Note:
eslint-plugin-jsdoc(38 TP / 37 FP / F1=66.1%) was excluded from the leaderboard. Its detections are incidental — it flags every function missing JSDoc, not security issues. A 97.4% false positive rate is unusable for security.
Vulnerable Code Detections (out of 40 patterns):
Interlace Ecosystem: ████████████████████████████████████████ 40 (100%)
eslint-plugin-unicorn: ██████████████████████░░░░░░░░░░░░░░░░░░ 22 (55%)
eslint-plugin-security: ███████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 11 (27.5%, current v4.0.1 — 0% on v2.1.1, crashed)
eslint-plugin-sonarjs: ██████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 10 (25%, current v4.1.0 — was 14/40 on v3.0.6)
@microsoft/eslint-plugin: ████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 4 (10%)
The "Interlace Ecosystem" row in the leaderboard is the combined output of 10 ESLint plugins running together against the same fixture suite — 201 rules in total:
eslint-plugin-secure-coding · eslint-plugin-node-security · eslint-plugin-browser-security · eslint-plugin-pg · eslint-plugin-jwt · eslint-plugin-mongodb-security · eslint-plugin-vercel-ai-security · eslint-plugin-lambda-security · eslint-plugin-express-security · eslint-plugin-nestjs-security
Per-plugin rule counts and focus areas are in Specialization vs. one-size-fits-all below.
The 100% number is the destination, not the starting point — and if I hid that, the first engineer to clone the repo would catch it and say so in the comments. So here is the whole arc, with the failing run on the table.
In the Feb-2026 baseline (2026-02-07.json), Interlace itself scored 31/40 with 9 false positives — F1 77.5%, run against the same fixtures as every competitor above. The 9 misses were concrete:
vuln_sql_string_concat— SQL built by string concatenation (the relational SQL rule only matched template literals)vuln_xss_innerhtml—innerHTMLassignment from a tainted sourcevuln_random_token,vuln_random_session—Math.random()used for security tokens (weak randomness)vuln_nosql_mongo,vuln_nosql_where— operator injection into a Mongo query /$wherevuln_ssrf_fetch,vuln_ssrf_axios— server-side request to a user-controlled URLvuln_redirect— open redirect from an unvalidatedLocation
And the 9 false positives were just as instructive — the rules were firing on safe code: validated child_process calls, allowlisted path joins, null-prototype objects, crypto.timingSafeEqual, same-origin redirects. A 1:1 TP:FP ratio is exactly the alert-fatigue trap I criticize SonarJS and unicorn for above; in Feb, Interlace was in it too.
Those two lists — the misses and the noise — were the rule backlog. The SSRF, NoSQL, and open-redirect gaps drove new detectors; the false positives drove the allowlist-aware refinements that let the safe patterns pass. The last two misses (vuln_random_token, vuln_random_session) closed when crypto/randomness rules were consolidated into eslint-plugin-node-security 4.2.0 (released 2026-05-10) with no-math-random-crypto. The golden verification run on 2026-05-29, with that version pinned, is the 40/40 / 0 FP / 100% you see in the leaderboard.
Two honest caveats so nobody is surprised:
- Pin the version. On
eslint-plugin-node-security< 4.2.0 the two randomness cases are still missed, so the fleet scores 38/40. The headline requires ≥ 4.2.0. - Run the fleet, not one plugin. A single plugin in isolation covers only its domain — a spot-run of
node-securityalone against all 40 fixtures lands around 27% (7/40), because it was never meant to catch SQL or JWT or XSS on its own. The 100% is the 10 plugins running together, which is how the config block wires them.
That's the difference between a benchmark you can trust and a screenshot you can't reproduce: the failing run is in the same folder as the passing one.
The fixture-tuning question, answered directly, not implied. I built the winning plugin and designed the fixtures, so the obvious objection is: did the 40 patterns get chosen because Interlace already caught them? The evidence I can actually point to: the Feb 77.5% and May 100% runs scored the same 40 fixture files — nothing in that suite changed between them. What changed is 76 lines of rule code closing 9 named misses. If the fixtures had been tuned to the rules, the Feb baseline wouldn't have had 9 misses to begin with — a rigged test doesn't fail its own designer 22.5% of the time. That's evidence the fixtures constrained the rules, not the reverse. I'm one person, not a lab — I can't hand you a third-party audit, and I'm not going to pretend that's coming. What I can hand you is the actual repo: clone it, read the fixture files yourself, write your own 41st pattern, and see if it survives. That's not a weaker claim than an audit — it's a different one, and it's the one actually available to you right now:
git clone https://github.com/ofri-peretz/eslint-benchmark-suite
cd eslint-benchmark-suite && npm install
# add your own fixture to benchmarks/fn-fp-comparison/fixtures/, then:
npm run benchmark:fn-fp
If you run this and get a different number, I want to hear about it — that's the actual mechanism for catching a rigged benchmark, not a credential.
F1 Score: 37.3% (current, v4.0.1) | F1 Score: 0% (v2.1.1, what most lockfiles still pin)
If you have ^2.1.0 in your package.json, you have the crashing version. It fails on ESLint 9 with:
TypeError: context.getScope is not a function
Rule: "security/detect-child-process"
That's due to the deprecated context.getScope() API removed in ESLint 9. If your lockfile is still on 2.1.1, this plugin provides zero security coverage under flat config — check now, don't wait for this article to convince you.
Upgrade to the current release (4.0.1) and the crash goes away. Coverage doesn't fully come back with it:
| Category | 4.0.1 | What It Still Misses |
|---|---|---|
| Path Traversal | 4/4 ✅ | — |
| ReDoS | 2/2 ✅ | — |
| Prototype Pollution | 2/3 | one assignment-based case |
| Command Injection | 2/4 | execSync, spawn with shell: true |
| XSS / eval | 1/4 | innerHTML, document.write, new Function |
| SQL + NoSQL Injection | 0/6 | ❌ All — every relational and document-store case |
| Hardcoded Credentials | 0/4 | ❌ All |
| JWT | 0/3 | ❌ All |
| Insecure Randomness | 0/2 | ❌ All |
| Weak Cryptography | 0/3 | ❌ All |
| Timing Attacks | 0/2 | ❌ All |
| SSRF | 0/2 | ❌ All |
| Open Redirect | 0/1 | ❌ All |
Eleven of 40, with 8 false positives alongside them. The category it's most associated with by name — SQL injection — is a complete miss, current version or not.
📖 Deep dive: eslint-plugin-security Is Unmaintained — Here's What Nobody Tells You
F1 Score: 36.4% (v4.1.0, current) | 10 detected, 30 missed, 5 false positives
Re-benchmarked against the current release before publishing — the version most comparisons would cite (3.0.6) actually scores higher (14/40, 35.0% recall) than what's on npm today:
| Category | SonarJS (4.1.0) | What It Missed |
|---|---|---|
| Insecure Randomness | 2/2 ✅ | — |
| Hardcoded Credentials | 2/4 | AWS keys, API keys |
| Weak Cryptography | 2/3 | DES |
| XSS / eval | 2/4 | innerHTML, document.write |
| JWT | 1/3 | missing-algorithm, no-expiry (caught only alg:none) |
| ReDoS | 1/2 | user-supplied pattern |
| Command Injection | 0/4 ❌ | Regression — was 4/4 on v3.0.6, now 0/4 |
| SQL + NoSQL Injection | 0/6 | ❌ All — every relational and document-store case |
| Path Traversal | 0/4 | ❌ All |
| Prototype Pollution | 0/3 | ❌ All |
| Timing Attacks | 0/2 | ❌ All |
| SSRF | 0/2 | ❌ All |
| Open Redirect | 0/1 | ❌ All |
The one category SonarJS still carries cleanly is Insecure Randomness. Despite having 269 rules (the most of any plugin tested), the current release (v4.1.0) catches 10/40 (25.0% recall) — down from 14/40 (35.0%) on v3.0.6, the version originally tested — and misses 75% of vulnerabilities, including 0 of 6 SQL/NoSQL injection cases (unchanged) and, new in this release, 0 of 4 command injection cases it used to catch completely. Many of its rules target code quality, not security.
When a developer would believe they're covered: SonarJS fires on real findings. You see command injection alerts, you fix them, you feel the tool is working. The SQL injection cases that SonarJS silently misses never appear in your lint output — so you never know they're there. The linter looks active; the gap is invisible.
📖 Deep dive: SonarJS vs Interlace: 269 Rules, 65% Missed
F1 Score: 17.8% | 4 detected, 36 missed, 1 false positive
Microsoft's SDL (Security Development Lifecycle) plugin found all four cases in the XSS/eval bucket — innerHTML, document.write, eval, and new Function — but missed everything else. Its 17 rules focus narrowly on browser-side injection. (In the fixture taxonomy these four split as 2 DOM-XSS + 2 code-execution; the benchmark scores them as one XSS/eval category, so this is the full 4/4 of that bucket and nothing beyond it.)
| Category | Microsoft SDL |
|---|---|
| XSS / eval | 4/4 ✅ |
| Everything else (36) | 0/36 ❌ |
When a developer would believe they're covered: Microsoft SDL is often installed alongside a broader security posture ("we follow the SDL"). Its name implies enterprise-grade coverage. Engineers who see XSS alerts in their feed assume the tool is catching the important things — and SQL injection, path traversal, and SSRF never come up because the rules don't exist.
📖 Deep dive: Microsoft SDL vs Interlace: Enterprise Security Benchmark
F1 Score: 9.5% | 2 detected, 0 false positives
Only 2 rules, but they do their job — detecting hardcoded secrets with zero false positives. Good as a supplement, but not a security strategy.
F1 Score: 9.3% | 2 detected, 1 false positive
Detects innerHTML and insertAdjacentHTML DOM sinks. Cannot recognize DOMPurify sanitization (1 FP). Useful for browser projects but covers only 2 of 13 categories.
These widely-installed plugins are not security tools, confirmed by zero detections:
| Plugin | Downloads | Purpose | Security Detections |
|---|---|---|---|
| eslint-plugin-react | 17M+ | React patterns | 0 |
| eslint-plugin-import | 40M+ | Module resolution | 0 |
| eslint-plugin-promise | 10M+ | Promise patterns | 0 |
| eslint-plugin-jest | 14M+ | Jest testing | 0 |
| eslint-plugin-vue | 7M+ | Vue.js | 0 |
| @angular-eslint | 2.25M+ | Angular | 0 |
| eslint-plugin-jsx-a11y | 14M+ | Accessibility | 0 |
These are excellent tools for their intended purpose. But if your security posture relies on them, you have zero coverage.
Every cell below is read straight from the
categoryBreakdownblocks of the two run files: the Interlace column fromgolden-2026-05-29.json, the five competitor columns from2026-02-07.json. SQL and NoSQL are scored as one SQL Injection (6) bucket here, exactly as the fixture taxonomy and line-96 table do — so the row totals reconcile to the leaderboard. If you clone the repo, these are the numbersnpm run benchmark:fn-fpprints.
| Category | Interlace | SonarJS (4.1.0) | MS SDL | Security (4.0.1) | no-unsanitized | no-secrets |
|---|---|---|---|---|---|---|
| SQL Injection (6) | ✅ 6/6 | ❌ 0/6 | ❌ 0/6 | ❌ 0/6 | ❌ 0/6 | ❌ 0/6 |
| Command Injection (4) | ✅ 4/4 | ❌ 0/4 | ❌ 0/4 | ⚠️ 2/4 | ❌ 0/4 | ❌ 0/4 |
| Path Traversal (4) | ✅ 4/4 | ❌ 0/4 | ❌ 0/4 | ✅ 4/4 | ❌ 0/4 | ❌ 0/4 |
| Hardcoded Credentials (4) | ✅ 4/4 | ⚠️ 2/4 | ❌ 0/4 | ❌ 0/4 | ❌ 0/4 | ⚠️ 2/4 |
| JWT (3) | ✅ 3/3 | ⚠️ 1/3 | ❌ 0/3 | ❌ 0/3 | ❌ 0/3 | ❌ 0/3 |
| XSS / eval (4) | ✅ 4/4 | ⚠️ 2/4 | ✅ 4/4 | ⚠️ 1/4 | ⚠️ 2/4 | ❌ 0/4 |
| Prototype Pollution (3) | ✅ 3/3 | ❌ 0/3 | ❌ 0/3 | ⚠️ 2/3 | ❌ 0/3 | ❌ 0/3 |
| Insecure Random (2) | ✅ 2/2 | ✅ 2/2 | ❌ 0/2 | ❌ 0/2 | ❌ 0/2 | ❌ 0/2 |
| Weak Crypto (3) | ✅ 3/3 | ⚠️ 2/3 | ❌ 0/3 | ❌ 0/3 | ❌ 0/3 | ❌ 0/3 |
| Timing Attacks (2) | ✅ 2/2 | ❌ 0/2 | ❌ 0/2 | ❌ 0/2 | ❌ 0/2 | ❌ 0/2 |
| SSRF (2) | ✅ 2/2 | ❌ 0/2 | ❌ 0/2 | ❌ 0/2 | ❌ 0/2 | ❌ 0/2 |
| Open Redirect (1) | ✅ 1/1 | ❌ 0/1 | ❌ 0/1 | ❌ 0/1 | ❌ 0/1 | ❌ 0/1 |
| ReDoS (2) | ✅ 2/2 | ⚠️ 1/2 | ❌ 0/2 | ✅ 2/2 | ❌ 0/2 | ❌ 0/2 |
| TOTAL | 40/40 | 10/40 | 4/40 | 11/40 | 2/40 | 2/40 |
eslint-plugin-securityandeslint-plugin-sonarjscolumns reflect their current releases (4.0.1 and 4.1.0), both re-benchmarked 2026-07-05 — see the version note above. The previously pinned versions score 0/40 (2.1.1, crashes before evaluating any fixture) and 14/40 (3.0.6 — higher than the current release; the difference is a lost Command Injection detector).
The reason Interlace achieves 100% coverage is specialization. Instead of one monolithic plugin trying to cover everything, the ecosystem uses 10 purpose-built plugins:
| Plugin | Focus | Rules |
|---|---|---|
eslint-plugin-secure-coding | Core OWASP patterns | 23 |
eslint-plugin-node-security | fs, child_process, vm, weak crypto, randomness | 42 |
eslint-plugin-browser-security | XSS, CORS, CSP | 45 |
eslint-plugin-pg | SQL injection, connection safety | 13 |
eslint-plugin-jwt | Algorithm confusion, token safety | 13 |
eslint-plugin-mongodb-security | NoSQL injection, operator injection | 16 |
eslint-plugin-vercel-ai-security | Prompt injection, output validation | 19 |
eslint-plugin-lambda-security | IAM, cold starts, secrets | 14 |
eslint-plugin-express-security | Helmet, CORS, sessions | 10 |
eslint-plugin-nestjs-security | Guards, pipes, decorators | 6 |
Crypto rules (weak algorithms, insecure randomness) were consolidated into
eslint-plugin-node-securityon 2026-05-10. The previously separateeslint-plugin-cryptopackage is deprecated and should not be installed.
Each plugin is maintained by domain experts and updated independently. A JWT vulnerability doesn't require updating the SQL injection rules.
Two years ago, the 40 patterns in this suite entered codebases at human typing speed — one developer, one risky line, occasionally. That constraint is gone. Your team now generates code with an LLM, and the model reproduces these exact patterns at machine speed, with the confidence of well-formatted, type-correct output.
This isn't speculation; I measured it. In a separate experiment I asked Claude (Haiku through Opus) to write common Node.js functions with no security context — 65–75% shipped with a vulnerability, and the rate was statistically consistent across every model size. The categories were the same OWASP families this benchmark scores: string-concatenated SQL, child_process with shell, unbounded regex, weak crypto. (the full breakdown.)
And before you assume this is one vendor's problem, it isn't. I ran the same security scoring across 700 functions from 5 models — three Claude tiers and two Gemini tiers — and every one of them shipped vulnerable code at a 49–73% rate. They just fail in different places: Claude Opus generated vulnerable JWT code in 7 out of 7 runs, while Gemini Flash got the exact same prompt perfect 7 out of 7 — and on other domains that ranking flips. There is no "safe model" you can switch to; the leaderboard you'd pick from is itself misleading. The benchmark doesn't care which model wrote the line — it scores the line. That model-independence is the whole point: a deterministic rule is the one part of this pipeline that doesn't have a bad day.
The model output is the new attack surface, and it walks straight past the human review that used to be the last line of defense — because it looks senior. I gave Claude one prompt for a NestJS users service and got 200 lines that TypeScript compiled clean; a specialized linter found 6 security holes in 3 seconds (the full breakdown). And asking the model to fix its own findings without deterministic feedback made it worse: it introduced brand-new vulnerability categories at 4× the rate — what I call the AI Hydra Problem: cut one head, two grow back.
The takeaway for this benchmark: a plugin that detects 0% or 35% (SonarJS v3.0.6; 25.0% on the current v4.1.0) of these patterns was already a liability. Pointed at AI-generated code that reintroduces the same patterns by the hundred, it's a rubber stamp on a vulnerability factory. The deterministic 100%-recall, 0%-FP layer is what gives the model an objective signal to converge against — and it's the same npm run benchmark:fn-fp command below, which you can rerun against your own AI's output, not just mine.
If you've read this far, close the gap in your own repo before you forget — two commands:
npm install -D eslint-plugin-secure-coding eslint-plugin-node-security eslint-plugin-browser-security
npx eslint . # against your last AI-generated PR, ideally
(Full flat-config block and the per-domain plugins are in Migrate in 60 Seconds below.)
There are two honest install paths here, and I'm labeling them so the command you paste matches the number you expect.
Starter (3 plugins) — the highest-traffic categories, not the headline 100%. This is the same block from the AI section above. It covers core OWASP patterns, Node sinks, and browser XSS — it does not cover SQL/NoSQL injection, JWT, or SSRF, which live in the other seven plugins below. Treat this as "meaningfully better than the incumbent," not a specific percentage — the right first move on most repos, not the finish line:
npm install -D eslint-plugin-secure-coding eslint-plugin-node-security eslint-plugin-browser-security
// eslint.config.js
import secureCoding from "eslint-plugin-secure-coding";
import nodeSecurity from "eslint-plugin-node-security";
import browserSecurity from "eslint-plugin-browser-security";
export default [
secureCoding.configs.recommended,
nodeSecurity.configs.recommended,
browserSecurity.configs.recommended,
];
Full fleet (10 plugins) — this is what reproduces the 40/40. The headline number is the whole fleet running together, with eslint-plugin-node-security pinned to ^4.2.0 (the version with no-math-random-crypto that closed the last two randomness cases — see the 77.5%→100% section). Three plugins alone do not get you to 100%; SQL, JWT, NoSQL, SSRF, and the framework-specific checks live in the other seven:
npm install -D \
eslint-plugin-secure-coding \
"eslint-plugin-node-security@^4.2.0" \
eslint-plugin-browser-security \
eslint-plugin-pg \
eslint-plugin-jwt \
eslint-plugin-mongodb-security \
eslint-plugin-vercel-ai-security \
eslint-plugin-lambda-security \
eslint-plugin-express-security \
eslint-plugin-nestjs-security
// eslint.config.js — the full 10-plugin fleet that scores 40/40
import secureCoding from "eslint-plugin-secure-coding";
import nodeSecurity from "eslint-plugin-node-security";
import browserSecurity from "eslint-plugin-browser-security";
import pg from "eslint-plugin-pg";
import jwt from "eslint-plugin-jwt";
import mongodbSecurity from "eslint-plugin-mongodb-security";
import vercelAiSecurity from "eslint-plugin-vercel-ai-security";
import lambdaSecurity from "eslint-plugin-lambda-security";
import expressSecurity from "eslint-plugin-express-security";
import nestjsSecurity from "eslint-plugin-nestjs-security";
export default [
secureCoding.configs.recommended,
nodeSecurity.configs.recommended, // pin ^4.2.0 for the 2 randomness cases
browserSecurity.configs.recommended,
pg.configs.recommended,
jwt.configs.recommended,
mongodbSecurity.configs.recommended,
vercelAiSecurity.configs.recommended,
lambdaSecurity.configs.recommended,
expressSecurity.configs.recommended,
nestjsSecurity.configs.recommended,
];
Run ESLint. See what you've been missing.
This article is the ecosystem overview. For the head-to-head per-plugin comparisons:
- SonarJS vs Interlace: 269 Rules Still Miss 65% of Vulnerabilities
- Microsoft SDL vs Interlace: Enterprise Security Benchmark
- eslint-plugin-security Is Unmaintained — Here's What Nobody Tells You
And for why this benchmark matters more every quarter — the AI angle:
- I Let Claude Write 80 Functions. 65–75% Had Security Vulnerabilities.
- Claude Inherited a NestJS Service. ESLint Found 6 Security Holes.
- The AI Hydra Problem: Fix One AI Bug, Get Two More
And the measurement foundations under the numbers — what a leaderboard rank does and doesn't tell you, and how a benchmark's own design can bias what it finds:
Full plugin docs: eslint.interlace.tools
Go check two things right now: what version of eslint-plugin-security your lockfile actually has (npm ls eslint-plugin-security), and whether it's still firing on ESLint 9. If you're on 2.1.1 or older, there's a real chance the answer is "it's been a green checkmark over nothing for months." If you're already on 4.0.1 — check whether it's catching SQL injection in your own repo. It isn't in this benchmark.
Which security ESLint plugin has your team standardized on — and have you actually verified it covers the vulnerability classes in your tech stack? The specific coverage gaps here (SQL injection blind spots in SonarJS, total crash in eslint-plugin-security) are the kind of thing that only surfaces when someone runs a benchmark. I want to know: has your team? And if you have, what did you find?
I'm Ofri Peretz, a Security Engineering Leader and the architect of the Interlace ESLint Ecosystem. I build static analysis standards that automate security and performance for Node.js fleets at scale.
ofriperetz.dev | LinkedIn | GitHub
Part of the Interlace ESLint ecosystem. Benchmark source on GitHub · Follow: Dev.to/ofri-peretz
