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.
The map: OWASP Top 10 (2021) → plugins → rules
| # | 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.
A04 and A06: where source analysis hands off to another control
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. -
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.
Why these survive code review (and why AI now ships them faster)
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.
What a finding looks like
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.
Build your config, layer by layer
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
Compatibility
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 \ |
| Module system | CommonJS — loads from {% raw %}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 |
Where this fits
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:
-
eslint-plugin-jwt— thealg:nonebypass (A07) and 12 more auth rules -
eslint-plugin-pg— SQL injection (A03), connection leaks, the N+1 insert loop -
search_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)
Links
- 📦 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.
What's the OWASP category you've watched a team claim on a security
questionnaire and then completely fail to control in the actual code — the gap
between the slide and the call site? 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.
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.
Top comments (0)