247 ESLint rules mapped to the OWASP Top 10 — but most teams automate coverage for only 7 of the 10 categories. Here's which 3 categories have zero or near-zero static analysis coverage in most Node.js projects.
Every "100% OWASP coverage by static analysis" slide is off by two — and the
vendor selling it is counting on you not opening the OWASP page to check. I've
built ten security ESLint plugins, and here's the number to your face:
static analysis genuinely catches 8 of the 10 web categories at the source.
The other 2, it cannot. Anyone claiming 10 is mapping a require-secure-defaults
rule to "Insecure Design" and hoping the auditor nods along.
"How do you address the OWASP Top 10?" is now a line item on every enterprise security questionnaire, and the box everyone reaches for is "100% covered." That 8-of-10 is the honest answer instead — and it's the more useful one, because the line between "control you can audit at the call site" and "compliance-theater slide" is exactly which two categories you stop pretending to cover. Get that line wrong and you ship a SOC 2 report that says "Insecure Design: covered" over a money-moving endpoint with no rate limit. I've signed off on that report. So have you.
No single plugin gets you the 8, either. SQL injection needs database-aware
rules; JWT attacks need token-aware rules; DOM XSS needs browser-aware rules. So
the map below spans ten domain-security plugins (part of the
Interlace ecosystem). Every rule carries a
CWE, and most findings carry the classic OWASP category the CWE rolls up to, so
the evidence is greppable, not hand-waved — and the count is whatever your
installed version actually ships, which you can read for yourself (one-liner at
the end), not a frozen number on a slide. (Yes, the URL says a number. It was
true the day I wrote it; the only count I trust is the one your CI prints from
your own node_modules. Don't take mine — or any vendor's — on faith.)
This is the web OWASP Top 10 (2021). The AI/LLM list is mapped separately in the OWASP LLM Top 10 piece — also honestly, 8 of 10.
Before the map, the insight most teams miss: A04 (Insecure Design) has zero genuinely automatable ESLint rules — it's a design problem, not a code problem. The rules listed for it in the table below (require-secure-defaults, no-missing-validation-pipe) nudge toward safer defaults, but they don't catch the actual A04 vulnerabilities: missing rate limits on money-moving endpoints, replayed workflows, trust boundaries in the wrong place. Those are architectural decisions that happen before a line of code is written.
Most teams don't know this. They install a lint config, see OWASP categories in the output, and assume they're covered. The gap between "we have ESLint" and "we have OWASP coverage" is 247 rules and 2 categories — and no npm package tells you this automatically.
Here's the distribution across all 10 categories (approximate, from the ecosystem at time of writing):
| Category | Representative rule count | Coverage type |
|---|---|---|
| A03 Injection | ~80 rules | Full (SQL, NoSQL, DOM, LDAP, eval) |
| A07 Auth Failures | ~40 rules | Full |
| A02 Crypto Failures | ~35 rules | Full |
| A05 Misconfiguration | ~30 rules | Full |
| A01 Broken Access Control | ~25 rules | Full |
| A08 Data Integrity | ~20 rules | Full |
| A09 Logging Failures | ~10 rules | Full |
| A10 SSRF | ~7 rules | Full |
| A06 Vulnerable Components | ~5 rules | Partial (source hygiene only — not CVE graph) |
| A04 Insecure Design | ~2 rules | Partial (defaults nudges — not design coverage) |
A03 alone has more rules than A04, A06, A09, and A10 combined. If your threat model includes Insecure Design, your linter can't help — and that's the category most enterprise security questionnaires ask about first.
Slack-quotable version: 247 ESLint rules for OWASP Top 10 — but A04 (Insecure Design) has 0 automatable rules. If your threat model includes it, your linter can't help.
The most common pattern I see: a team picks up eslint-plugin-security (or a bundle config), sees it run in CI, and ticks the OWASP box. They never check which categories are actually covered, because nothing tells them.
The gap between "we have ESLint" and "we have OWASP coverage" is 247 rules and 2 uncovered categories — and no npm package tells you this automatically. The only way to know is to do what this article does: map each rule's CWE to its OWASP category and count.
If you want to verify your own install right now, skip to the one-liner at the bottom. It counts every rule across every installed Interlace plugin — the only count that matters is the one your CI prints.
| # | Category | Plugins | Representative rules |
|---|---|---|---|
| A01 | Broken Access Control | secure-coding, nestjs-security, lambda-security | no-missing-authentication, require-guards, no-missing-authorization-check |
| A02 | Cryptographic Failures | node-security, jwt | no-weak-hash-algorithm, no-ecb-mode, no-weak-secret |
| A03 | Injection | pg, mongodb-security, secure-coding, browser-security | no-unsafe-query, no-operator-injection, no-innerhtml |
| A04 | Insecure Design (partial) | secure-coding, nestjs-security | require-secure-defaults, no-missing-validation-pipe |
| A05 | Security Misconfiguration | express-security, browser-security, pg | require-helmet, require-csp-headers, no-unsafe-search-path |
| A06 | Vulnerable Components (partial) | node-security | detect-suspicious-dependencies, require-dependency-integrity, lock-file |
| A07 | Authentication Failures | jwt, secure-coding, express-security | no-algorithm-none, no-algorithm-confusion, no-insecure-cookie-options |
| A08 | Data Integrity Failures | secure-coding, node-security | no-unsafe-deserialization, no-zip-slip, no-unsafe-dynamic-require |
| A09 | Logging Failures | secure-coding, lambda-security | no-pii-in-logs, no-env-logging, no-error-swallowing |
| A10 | SSRF | node-security, lambda-security, browser-security | no-ssrf, no-user-controlled-requests, require-url-validation |
Each row lists representative rules, not the whole set — A03 alone spans SQL,
NoSQL, LDAP, XPath, and DOM injection across the four plugins above, plus
command/eval (node-security) and prompt injection (the AI layer) elsewhere in
the ecosystem.
This is where "100% OWASP coverage" decks lie. Two of the ten are not source patterns at a call site, so no source linter — this one included — fully covers them. Naming the right control instead of faking a rule is what a security reviewer actually wants:
- A04 Insecure Design — a missing rate limit on a money-moving endpoint, a
trust boundary in the wrong place, a workflow that can be replayed. That's an
architectural problem. A few rules nudge toward safe defaults
(
require-secure-defaults,no-missing-validation-pipe), but the real control is threat modeling and design review, not a linter. The rules that exist for A04 catch symptoms of poor defaults — they don't catch the absence of a rate limit or a broken trust boundary. That's the gap most teams assume is covered. - A06 Vulnerable & Outdated Components — a transitive dependency with a
published CVE. That's a Software Composition Analysis problem.
node-securitycovers the source-hygiene slice — suspicious install scripts (detect-suspicious-dependencies), integrity/lockfile drift (require-dependency-integrity,lock-file) — but the CVE graph itself belongs tonpm audit, Dependabot, or Snyk. Use both; they answer different questions.
Anyone selling you "automated 100% OWASP" is mapping a defaults rule to "Insecure Design" and hoping you don't open the OWASP page. You should.
None of the 8 categories above are exotic. They survive review for boring, human reasons — and the same reasons are now amplified by the assistant in your editor.
Take A03/A07. The reason client.query('SELECT * FROM t WHERE id = ' + id) and
jwt.verify(token, secret) without an algorithms allowlist sail through PR
review is that they look like the happy path. The string concatenation reads
as "build a query." The verify call reads as "check the token." A reviewer
skimming a 600-line diff at 5pm pattern-matches on intent, not on the trust
boundary — and both lines do what they appear to do, right up until someone
sends id = 1 OR 1=1 or a token with "alg":"none". There's no red flag in
the syntax; the vulnerability is in what's missing (a parameter slot, an
algorithm list), and humans are bad at reviewing absence.
I've watched a staff engineer — someone who would catch this instantly on a
whiteboard — approve exactly that jwt.verify line, because in a diff it was one
green-looking call surrounded by forty lines of legitimate refactor. The bug
wasn't a knowledge gap. It was attention budget. That's why "just train people to
review for security" never holds: it asks every reviewer to spot the absence of a
parameter, on every PR, forever, while tired. A rule never gets tired.
Now point an AI assistant at the same code. Ask it to "add an endpoint that looks up a user by ID" and a large share of models will hand you the concatenated query — because they were trained on the same Stack Overflow answers that shipped it for fifteen years. I let Claude write 80 functions and 65–75% carried a security vulnerability; when I handed it a clean NestJS service, the linter still found 6 holes the model reintroduced. The assistant doesn't make new classes of mistake — it makes the same OWASP-category mistakes, at the speed of autocomplete, and it makes them look even more idiomatic than the human version did. And it's not one rogue model: when I ranked five frontier models by the security of the code they wrote, every one of them leaked — a 49–73% vulnerability rate across all five, with auth-failure patterns alone showing up in 29–50% of generated functions. The OWASP categories below are where all of them leak, not a quirk of one vendor. Worse, the fix gets re-broken: patch one AI-suggested injection and the next prompt cheerfully reintroduces it, the "AI hydra" problem.
That's the actual case for mapping this to CI rather than to a checklist. A
human reviewer gets tired and a checklist gets stale, but a rule that fires on
OWASP:A03-Injection at the call site doesn't care whether a person or a model
typed the line. The rules below are the same whether the author has a pulse.
Findings are deterministic strings — each carries the CWE, the OWASP category the CWE rolls up to, a CVSS, the severity, and the compliance tags, then the fix:
src/db/tenants.ts
8:15 error 🔒 CWE-426 OWASP:A05-Security CVSS:7.5 | Unsafe "SET search_path" detected. | CRITICAL [SOC2,PCI-DSS]
Fix: Do not use dynamic values for search_path. Use static strings or strict validation.
src/app/chat/route.ts
6:11 error 🔒 CWE-74 OWASP:A03-Injection CVSS:9 | User input "userMessage" passed directly to generateText prompt without validation | CRITICAL [SOC2,GDPR]
Fix: Validate input before use: generateText({ prompt: validateInput(userInput) })
The inline OWASP:Axx tag is the classic web category the rule's CWE maps to
(CWE-74 → A03 Injection). Because it's a stable token, you can turn a lint run
into audit evidence — a coverage count per OWASP category:
// npx eslint . --format json > security-report.json
const report = require("./security-report.json");
const byCategory = report
.flatMap((file) => file.messages)
.map((m) => m.message.match(/OWASP:(A\d+)/)?.[1])
.filter(Boolean)
.reduce((acc, cat) => ((acc[cat] = (acc[cat] || 0) + 1), acc), {});
console.log("OWASP findings by category:", byCategory);
// → { A03: 12, A05: 4, A02: 2, ... }
That JSON is the artifact you hand the auditor — not a slide.
Don't install everything. Start with the core, then add the plugins that match
your stack. configs is a named export on every plugin (the default export
is the plugin object):
# core + the specialized layers you actually run — pick your manager
npm install --save-dev eslint-plugin-secure-coding eslint-plugin-node-security eslint-plugin-jwt eslint-plugin-pg
yarn add -D eslint-plugin-secure-coding eslint-plugin-node-security eslint-plugin-jwt eslint-plugin-pg
pnpm add -D eslint-plugin-secure-coding eslint-plugin-node-security eslint-plugin-jwt eslint-plugin-pg
bun add -d eslint-plugin-secure-coding eslint-plugin-node-security eslint-plugin-jwt eslint-plugin-pg
// eslint.config.js — flat config
import { configs as secureCoding } from "eslint-plugin-secure-coding";
import { configs as nodeSecurity } from "eslint-plugin-node-security";
import { configs as jwt } from "eslint-plugin-jwt";
import { configs as pg } from "eslint-plugin-pg";
export default [
secureCoding.recommended, // A01/A03/A04/A08/A09 — general source patterns
nodeSecurity.recommended, // A02/A06/A08/A10 — crypto, supply-chain, SSRF
jwt.recommended, // A02/A07 — token & signature security
// scope database rules to where queries live
{ files: ["**/db/**", "**/repositories/**"], ...pg.recommended },
];
Name the file
eslint.config.mjsif yourpackage.jsonisn't"type": "module"— theimportsyntax above needs ESM (the plugins themselves are CommonJS and load fine either way via Node's CJS↔ESM interop).
Add eslint-plugin-browser-security (frontend, A03/A05/A07), then
eslint-plugin-express-security / eslint-plugin-nestjs-security /
eslint-plugin-lambda-security / eslint-plugin-mongodb-security as your
backend stack dictates. eslint-plugin-vercel-ai-security adds the LLM layer —
see its honest OWASP-LLM map.
eslint-plugin-cryptois deprecated — its weak-algorithm / insecure-random rules were consolidated intoeslint-plugin-node-security. Install node-security, not crypto.
# CI — fail the PR on any new OWASP-tagged finding
- run: npx eslint . --max-warnings 0
Want the exact rule count your install ships, instead of trusting a number in a
title? Count it from your own node_modules — this is the only count that
matters, because it's the one running in your CI:
# rules across every interlace security plugin you've installed
for p in secure-coding node-security jwt pg mongodb-security \
browser-security express-security nestjs-security \
lambda-security vercel-ai-security; do
node -e "try{console.log(Object.keys(require('eslint-plugin-'+process.argv[1]).rules).length)}catch{console.log(0)}" "$p"
done | paste -sd+ - | bc
Every plugin in the map ships the same contract:
| Surface | Support |
|---|---|
| Package managers | npm, yarn, pnpm, bun — plain dev dependencies |
| Node | >= 18.0.0 |
| ESLint | ^8.0.0 || ^9.0.0 || ^10.0.0, flat config |
| Module system | CommonJS — loads from eslint.config.js or .mjs |
| Targets | AST-based — they read your source; the framework/driver peer is optional, never a runtime dep |
| Oxlint | flagship rules wired via the interlace-* ports with ESLint↔Oxlint parity gated in CI; full sets run ESLint-first |
This is the ecosystem-level OWASP view — the index page of a larger series. Each category above has a deep-dive that walks the full rule set, the attack behind it, and the code that survived review:
- The 30-minute security audit: a static analysis protocol for onboarding — how to run this map as a structured audit from day one
- Benchmark: 17 ESLint security plugins compared — how the Interlace plugins compare to every alternative
eslint-plugin-jwt— thealg:nonebypass (A07) and 12 more auth ruleseslint-plugin-pg— SQL injection (A03), connection leaks, the N+1 insert loopsearch_pathhijacking — the A05 attack (CWE-426) most teams have never heard of- I let Claude write 80 functions — what these rules catch when the author is a model, not a person (65–75% had a vuln)
- We ranked 5 AI models by security — 700 functions, every model leaking 49–73%, scored by 332 of these rules
- OWASP LLM Top 10 — the AI list, mapped just as honestly (also 8 of 10)
- 📦 npm: eslint-plugin-secure-coding (core) · node-security · jwt · pg
- 📖 Full rule docs (per-rule CWE + OWASP)
- 🔐 OWASP Top 10 (2021)
- 💻 Source on GitHub — the full Interlace plugin ecosystem
⭐ Star on GitHub if this is on your roadmap.
Which OWASP Top 10 category are you least confident your CI covers — and have you verified it with actual rule-to-category mapping? I'll start: I've lost count of the "Insecure Design: covered ✅" answers sitting on top of a money-moving endpoint with no rate limit. Tell me yours.
Part of the Interlace ESLint ecosystem. Source on GitHub · Follow: Dev.to/ofri-peretz