Four lines, four CVEs, zero compiler complaints:
crypto.createHash("md5").update(password).digest("hex"); // broken hash (CWE-327)
exec(`convert ${req.query.file} out.png`); // command injection (CWE-78)
await unzipper.extract({ path: dest }); // Zip Slip path traversal (CWE-22)
const token = Math.random().toString(36).slice(2); // predictable token (CWE-338)
Every one of these is a property of the Node.js standard library — crypto,
child_process, fs, Math — used the easy way instead of the safe way. They
pass type-checking. They pass unit tests (the test feeds trusted input). Then
they ship, and a researcher finds them with grep.
eslint-plugin-node-security reads those call sites and fails CI on the
dangerous shape. It's 34 rules spanning weak crypto, command/eval injection,
filesystem traversal, SSRF, supply-chain, and secrets-at-rest — each pinned to a
CWE, CVSS, and compliance tags. (It also absorbed the deprecated
eslint-plugin-crypto — all the cipher/hash/randomness rules live here now.)
This guide walks the crypto footguns, command injection, Zip Slip, the full 34-rule map, and exact install/engine support.
- 34 rules, each carrying a
CWEid, CVSS, and compliance tags (PCI-DSS / HIPAA / SOC2 / …). - 2 presets:
recommended(20 rules, mixed severity — the production baseline) andstrict(all 34 as errors). - Flat-config, CommonJS, ESLint
8 || 9 || 10, Node>= 18. AST-based — it lints source; no runtime peers. The formereslint-plugin-cryptois consolidated here (deprecated → usenode-security).
Node's crypto API will happily hand you broken primitives. Several rules,
mostly CWE-327 (broken/risky algorithm), draw the line:
// ❌ no-weak-hash-algorithm (CWE-327) — MD5/SHA-1 are collision-broken
crypto.createHash("md5").update(data);
// ❌ no-ecb-mode (CWE-327) — ECB leaks plaintext structure
crypto.createCipheriv("aes-256-ecb", key, null);
// ❌ no-static-iv (CWE-329) — a fixed IV destroys CBC/GCM security
crypto.createCipheriv("aes-256-gcm", key, FIXED_IV);
// ❌ no-math-random-crypto (CWE-338) — Math.random() is not a CSPRNG
const token = Math.random().toString(36).slice(2);
// ✅
crypto.createHash("sha256").update(data);
crypto.createCipheriv("aes-256-gcm", key, crypto.randomBytes(12)); // unique IV
const token = crypto.randomBytes(32).toString("hex"); // CSPRNG
The companion rules cover the rest of the surface: no-weak-cipher-algorithm,
no-sha1-hash, no-insecure-rsa-padding, no-deprecated-cipher-method,
no-insecure-key-derivation (CWE-916, e.g. low-iteration PBKDF2),
no-timing-unsafe-compare (CWE-208, use crypto.timingSafeEqual),
no-self-signed-certs (CWE-295), and no-cryptojs / prefer-native-crypto
(CWE-1104, prefer the audited native module over crypto-js).
// ❌ shell string interpolation = command injection
import { exec } from "node:child_process";
exec(`convert ${req.query.file} out.png`); // file="x.png; rm -rf /"
// ✅ no shell, arguments as an array — the rule's own fix
import { execFile } from "node:child_process";
execFile("convert", [req.query.file, "out.png"], { shell: false });
exec/execSync run their argument through /bin/sh, so any user-controlled
substring is shell code. execFile/spawn with an args array and
shell: false pass arguments directly to the binary — there's no shell to
inject into. detect-eval-with-expression (CWE-95) and no-dynamic-require
(CWE-94) close the analogous eval/require() holes.
Extracting an archive without validating entry paths lets a crafted entry
(../../../../etc/cron.d/x) write outside the destination directory:
// ❌ no-zip-slip (CWE-22) — entry path is trusted
zip.extractAllTo(dest);
// ✅ resolve each entry and confirm it stays under dest
const target = path.resolve(dest, entry.name);
if (!target.startsWith(path.resolve(dest) + path.sep))
throw new Error("Zip Slip");
detect-non-literal-fs-filename and no-arbitrary-file-access (both CWE-22)
catch the broader "user input reaches an fs path" pattern;
no-toctou-vulnerability (CWE-367) catches the check-then-use race.
All 34, grouped, with each rule's declared CWE:
| Rule | CWE |
|---|---|
no-weak-hash-algorithm | CWE-327 |
no-sha1-hash | CWE-327 |
no-weak-cipher-algorithm | CWE-327 |
no-ecb-mode | CWE-327 |
no-insecure-rsa-padding | CWE-327 |
no-deprecated-cipher-method | CWE-327 |
no-static-iv | CWE-329 |
no-insecure-key-derivation | CWE-916 |
no-timing-unsafe-compare | CWE-208 |
no-self-signed-certs | CWE-295 |
no-math-random-crypto | CWE-338 |
no-cryptojs-weak-random | CWE-338 |
no-cryptojs | CWE-1104 |
prefer-native-crypto | CWE-1104 |
| Rule | CWE |
|---|---|
detect-child-process | CWE-78 |
detect-eval-with-expression | CWE-95 |
no-unsafe-dynamic-require | CWE-95 |
no-dynamic-require | CWE-94 |
| Rule | CWE |
|---|---|
no-zip-slip | CWE-22 |
detect-non-literal-fs-filename | CWE-22 |
no-arbitrary-file-access | CWE-22 |
no-toctou-vulnerability | CWE-367 |
no-buffer-overread | CWE-126 |
no-deprecated-buffer | CWE-676 |
| Rule | CWE |
|---|---|
no-ssrf | CWE-918 |
detect-suspicious-dependencies | CWE-506 |
lock-file | CWE-829 |
require-dependency-integrity | CWE-494 |
no-dynamic-dependency-loading | CWE-1104 |
| Rule | CWE |
|---|---|
require-secure-credential-storage | CWE-312 |
require-storage-encryption | CWE-312 |
no-data-in-temp-storage | CWE-312 |
require-secure-deletion | CWE-459 |
no-pii-in-logs | CWE-359 |
That's all 34 (14 + 4 + 6 + 5 + 5). recommended turns on 20 of them (criticals
as errors, a few as warnings); strict turns on all 34.
# npm
npm install --save-dev eslint-plugin-node-security
# yarn
yarn add --dev eslint-plugin-node-security
# pnpm
pnpm add --save-dev eslint-plugin-node-security
# bun
bun add --dev eslint-plugin-node-security
Flat config (eslint.config.js):
// `configs` is a NAMED export; the default export is the plugin object.
import { configs } from "eslint-plugin-node-security";
export default [
configs.recommended, // 20 rules — production baseline
// configs.strict, // all 34 as errors
];
Run it — findings carry the CWE, OWASP category, CVSS, compliance tags, and fix:
src/auth/hash.ts
4:3 error 🔒 CWE-327 OWASP:A04-Cryptographic CVSS:7.5 | Use of weak hash algorithm: MD5. MD5 is cryptographically broken and unsuitable for security purposes. | CRITICAL [PCI-DSS,HIPAA,ISO27001,NIST-CSF]
Fix: Replace with sha256: crypto.createHash("sha256").update(data)
| Surface | Support |
|---|---|
| Package managers | npm, yarn, pnpm, bun — plain dev dependency |
| Node | >= 18.0.0 |
| ESLint | ^8.0.0 || ^9.0.0 || ^10.0.0, flat config |
| Module system | CommonJS — loads from both eslint.config.js and eslint.config.mjs |
| Runtime peers | None — it lints source AST |
| Replaces | eslint-plugin-crypto (deprecated) — its cipher/hash/randomness rules are consolidated here |
| Oxlint | Loads under Oxlint's JS-plugin runner via the interlace-node-security port, with ESLint↔Oxlint parity gated in CI. The full 34-rule set runs on ESLint today. |
- Source patterns, not runtime. It flags
createHash("md5"),exec(\…${x}`), and an unguardedextract()`. It can't confirm the key in your KMS is rotated or that your archive source is trusted — it removes the "we shipped MD5 / a shell string" failure mode at the call site. - Taint detection has edges. The injection and fs rules track user input toward a sink with configurable patterns; tune them rather than assuming the defaults are exhaustive, and pair with runtime input validation.
The generic security linters flag a few of these (eval, obvious child_process),
but they don't carry the CWE/CVSS/compliance metadata a security or audit
reviewer needs, and they don't cover the crypto surface at this depth.
eslint-plugin-node-security is the dedicated Node.js-stdlib layer — crypto,
injection, filesystem, SSRF, supply-chain, secrets — and the consolidation home
for the retired crypto plugin. It's the runtime-foundation member of the
Interlace family, underneath the
framework-specific plugins (-express-security, -nestjs-security, …).
⭐ Star on GitHub if your Node.js code does any of the above.
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. eslint-plugin-node-security
is its Node.js-standard-library layer.