I took one file with 12 classes of real vulnerabilities and ran it through
four linter configurations — two engines (ESLint, Oxlint) crossed with the rules
you'd actually reach for. The detection spread on the same file:
- Oxlint built-in rules: 1 finding
- Interlace flagship rules (on Oxlint): 5 — the portability subset (see below)
- eslint-plugin-security: 21
- Interlace plugins (on ESLint): 46
The takeaway isn't "tool X wins." It's that the engine is a commodity and the
rules are the product — and the rules you pick should run on whichever engine
you choose. Here's the data, the false positives, and the parity proof.
The four configurations
- Oxlint built-in — the fast Rust engine's own rules, no security plugin.
- eslint-plugin-security — the generic incumbent (~1.6M weekly), on ESLint.
-
Interlace @ ESLint — the domain plugins (
secure-coding,node-security,pg,browser-security) combined, so the scope fairly matches the incumbent's monolith. - Interlace @ Oxlint — the same Interlace flagship rules, loaded into Oxlint's JS-plugin runner.
Detection — vulnerable.js (12 vulnerability classes)
| Config | Engine | Findings | Notes |
|---|---|---|---|
| Oxlint built-in | Oxlint | 1 |
no-eval only |
| Interlace flagship (3 wired rules) | Oxlint | 5 | the rules that are parity-wired |
| eslint-plugin-security (recommended) | ESLint | 21 | the classic generic patterns |
| Interlace (4 plugins, recommended) | ESLint | 46 | across 20 distinct rules |
Oxlint's native ruleset caught a single security issue (no-eval) — because
Oxlint is an engine, not a security ruleset. You pick it for speed, not
coverage. The generic incumbent caught the well-known 21. The domain plugins, run
together, caught 46 across 20 rules — SQL injection (pg/no-unsafe-query),
unsafe deserialization, ReDoS, weak hashing, Math.random() for crypto, unsafe
innerHTML, insecure comparisons, and more that a generic ruleset has no rule
for.
False positives — safe-patterns.js (validated-safe code)
Detection only counts if precision holds. Run against a file of deliberately
safe patterns:
| Config | False positives | On what |
|---|---|---|
| Oxlint built-in | 0 | — |
| Interlace @ Oxlint | 0 | — |
| Interlace @ ESLint | 3 |
pg/no-select-all (a perf/clarity rule) ×2, conservative browser-security/no-innerhtml ×1 |
| eslint-plugin-security | 5 |
detect-object-injection on allowlist-validated keys ×3, detect-non-literal-fs-filename on path-validated reads ×2 |
This is the honest difference. The incumbent's 5 are genuine false positives
— it flags obj[key] even after VALID_KEYS.includes(key), and
fs.readFileSync(p) even after path.basename + startsWith validation,
because it pattern-matches the sink without seeing the guard. The Interlace "3"
aren't security false positives: no-select-all is a performance/clarity rule
firing on SELECT *, and no-innerhtml is conservative by design (it flags
innerHTML even when the value is DOMPurify-sanitized — a deliberate choice you
can disable with a documented comment).
The parity proof: the same rule, both engines
This is the part that matters most. The Interlace flagship rules don't just also
exist on Oxlint — they emit the identical CWE-tagged finding on both
engines. pg/no-unsafe-query on the same line, under ESLint and under Oxlint:
🔒 CWE-89 OWASP:A03-Injection CVSS:9.8 | Unsafe SQL query detected. Variable interpolation found. | CRITICAL [SOC2,PCI-DSS,NIST-CSF]
Fix: Use parameterized queries ($1, $2) instead of string concatenation. | https://node-postgres.com/features/queries#parameterized-queries
Same CWE, same OWASP category, same CVSS, same compliance tags — character for
character. So you are not
locked to an engine: run the full domain set on ESLint today, run the
flagship rules on Oxlint for editor-speed feedback, and get the same security
signal either way. (The full 119-rule set is ESLint-first; the flagship rules are
the ones wired + parity-gated on Oxlint so far.)
How to read this (it's a landscape, not a leaderboard)
- Oxlint is the right call when you want a fast engine — pair it with real security rules, because its built-in set isn't one.
- eslint-plugin-security is the familiar generic floor; it catches the classics but pattern-matches sinks, so it costs you false positives on validated code.
- The domain plugins add depth (database, crypto, DOM, deserialization) the generic set has no rules for — that's the gap, not a verdict.
- Portability is the point: pick rules that run on both engines so the engine decision stays a performance choice, not a coverage lock-in.
Methodology — reproduce it
Honest disclosure: the fixtures are team-authored (vulnerable.js, 12
vulnerability classes; safe-patterns.js, validated-safe patterns), so these
numbers measure coverage of the surface we designed the rules around — run it on
your own code for an unbiased read. Versions: eslint@9.39.4, oxlint@1.67.0,
eslint-plugin-secure-coding@3.2.0 (27 rules), node-security@4.2.0 (34),
pg@1.4.3 (13), browser-security@1.2.3 (45), eslint-plugin-security@4.0.0.
Method: each plugin's recommended preset, --format json, counted by ruleId
(restricted to the four plugins' rule IDs — a raw run also surfaces a couple of
core/TypeScript notices from the fixture).
The fixtures and the eslint-plugin-security config live in the repo's
packages/eslint-plugin-secure-coding/benchmark/;
the flagship Oxlint config is .oxlintrc.flagship.json at the repo root.
The Interlace side combines the four plugins (fair scope vs the monolith) in one
flat config:
// eslint.config.mjs
import secureCoding from "eslint-plugin-secure-coding";
import nodeSecurity from "eslint-plugin-node-security";
import pg from "eslint-plugin-pg";
import browserSecurity from "eslint-plugin-browser-security";
export default [
{
files: ["**/*.js"],
plugins: {
"secure-coding": secureCoding,
"node-security": nodeSecurity,
pg,
"browser-security": browserSecurity,
},
rules: {
...secureCoding.configs.recommended.rules,
...nodeSecurity.configs.recommended.rules,
...pg.configs.recommended.rules,
...browserSecurity.configs.recommended.rules,
},
},
];
# install the engines + the plugins
npm i -D eslint@9 oxlint eslint-plugin-secure-coding eslint-plugin-node-security \
eslint-plugin-pg eslint-plugin-browser-security eslint-plugin-security
# 1) Interlace @ ESLint — the eslint.config.mjs above
npx eslint test-files/vulnerable.js --format json
# 2) the incumbent — same shape, one plugin:
# plugins: { security }, rules: { ...security.configs.recommended.rules }
npx eslint --config eslint.config.security.mjs test-files/vulnerable.js --format json
# 3) Oxlint built-in (npm-installed — reproducible as-is)
npx oxlint test-files/vulnerable.js
Config 4 (Interlace @ Oxlint) is the only step that needs the repo rather
than npm: the interlace-* Oxlint shims load each plugin's built dist/, so
reproduce it from a clone —
git clone https://github.com/ofri-peretz/eslint && npx turbo run build, then
npx oxlint -c .oxlintrc.flagship.json <file> from the repo root.
Compatibility
| Surface | Support |
|---|---|
| Package managers | npm, yarn, pnpm, bun |
| Node | >= 18.0.0 |
| ESLint | `^8.0.0 \ |
| Oxlint | flagship rules run via the {% raw %}interlace-* JS-plugin ports, ESLint↔Oxlint parity-gated in CI |
| Module system | Plugins ship CommonJS; your config can be eslint.config.js or .mjs
|
# the four plugins benchmarked here
npm install --save-dev eslint-plugin-secure-coding eslint-plugin-node-security eslint-plugin-pg eslint-plugin-browser-security
# yarn add -D … · pnpm add -D … · bun add -d …
Links
- 📦 secure-coding · node-security · pg · browser-security
- 📖 Full rule docs (per-rule CWE)
- 💻 Source + benchmark on GitHub
⭐ Star on GitHub if you'd rather run security rules that aren't locked to one engine.
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)