DEV Community

Cover image for I Mapped the OWASP Top 10 to ESLint Rules. 8 Hold Up. 2 Are Vendor Theater.
Ofri Peretz
Ofri Peretz

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

I Mapped the OWASP Top 10 to ESLint Rules. 8 Hold Up. 2 Are Vendor Theater.

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-security covers the source-hygiene slice — suspicious install scripts (detect-suspicious-dependencies), integrity/lockfile drift (require-dependency-integrity, lock-file) — but the CVE graph itself belongs to npm 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) })
Enter fullscreen mode Exit fullscreen mode

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, ... }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
// 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 },
];
Enter fullscreen mode Exit fullscreen mode

Name the file eslint.config.mjs if your package.json isn't
"type": "module" — the import syntax 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-crypto is deprecated — its weak-algorithm / insecure-random
rules were consolidated into eslint-plugin-node-security. Install
node-security, not crypto.

# CI — fail the PR on any new OWASP-tagged finding
- run: npx eslint . --max-warnings 0
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:


Links

⭐ 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.

ofriperetz.dev · LinkedIn · GitHub

Top comments (0)