DEV Community

Cover image for MD5, exec(), and Zip Slip: 34 ESLint Rules That Fail Your Node.js CI Before They Ship.
Ofri Peretz
Ofri Peretz

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

MD5, exec(), and Zip Slip: 34 ESLint Rules That Fail Your Node.js CI Before They Ship.

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

Every one of these is a property of the Node.js standard librarycrypto,
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.


TL;DR

  • 34 rules, each carrying a CWE id, CVSS, and compliance tags (PCI-DSS / HIPAA / SOC2 / …).
  • 2 presets: recommended (20 rules, mixed severity — the production baseline) and strict (all 34 as errors).
  • Flat-config, CommonJS, ESLint 8 || 9 || 10, Node >= 18. AST-based — it lints source; no runtime peers. The former eslint-plugin-crypto is consolidated here (deprecated → use node-security).

The crypto footguns

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);
Enter fullscreen mode Exit fullscreen mode
// ✅
crypto.createHash("sha256").update(data);
crypto.createCipheriv("aes-256-gcm", key, crypto.randomBytes(12)); // unique IV
const token = crypto.randomBytes(32).toString("hex"); // CSPRNG
Enter fullscreen mode Exit fullscreen mode

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


Command injection — detect-child-process (CWE-78)

// ❌ shell string interpolation = command injection
import { exec } from "node:child_process";
exec(`convert ${req.query.file} out.png`); // file="x.png; rm -rf /"
Enter fullscreen mode Exit fullscreen mode
// ✅ 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 });
Enter fullscreen mode Exit fullscreen mode

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.


Zip Slip — no-zip-slip (CWE-22)

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);
Enter fullscreen mode Exit fullscreen mode
// ✅ 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");
Enter fullscreen mode Exit fullscreen mode

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.


The full rule set

All 34, grouped, with each rule's declared CWE:

Cryptography

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

Injection / dynamic execution

Rule CWE
detect-child-process CWE-78
detect-eval-with-expression CWE-95
no-unsafe-dynamic-require CWE-95
no-dynamic-require CWE-94

Filesystem & buffers

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

SSRF & supply chain

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

Secrets & data-at-rest

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.


Install

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

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

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

Compatibility

Surface Support
Package managers npm, yarn, pnpm, bun — plain dev dependency
Node >= 18.0.0
ESLint `^8.0.0 \
Module system CommonJS — loads from both {% raw %}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.

What it does — and doesn't — see

  • Source patterns, not runtime. It flags createHash("md5"), exec(\…${x}), and an unguarded extract(). 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.

Where this sits in the ecosystem

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, …).


Links

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

ofriperetz.dev · LinkedIn · GitHub

Top comments (0)