DEV Community

Cover image for Your ESLint Security Plugin is Missing 80% of Vulnerabilities (I Have Proof)
Ofri Peretz
Ofri Peretz

Posted on

Your ESLint Security Plugin is Missing 80% of Vulnerabilities (I Have Proof)

I ran a rigorous benchmark comparing the two major ESLint security plugins. This article covers the full methodology, test files, and resultsβ€”including why 0 false positives matters more than raw speed.

Benchmark Methodology

The Test Files

vulnerable.js (218 lines) - Contains 12 categories of real vulnerabilities:

// 1. Command Injection
exec(`ls -la ${userInput}`);
execSync('echo ' + userInput);
spawn('bash', ['-c', userInput]);

// 2. Path Traversal
fs.readFile(filename, 'utf8', callback);
fs.readFileSync(filename);

// 3. Object Injection
obj[key] = value;
data[key][value] = 'test';

// 4. SQL Injection
db.query('SELECT * FROM users WHERE id = ' + userId);

// 5. Code Execution
eval(code);
new Function(code);

// 6. Regex DoS
const evilRegex = /^(a+)+$/;
new RegExp(userInput);

// 7. Weak Cryptography
crypto.createHash('md5').update(password);
Math.random().toString(36);

// 8. Timing Attacks
if (inputToken === storedToken) {
  return true;
}

// 9. XSS
document.getElementById('output').innerHTML = userContent;

// 10. Insecure Cookies
document.cookie = `${name}=${value}`;

// 11. Dynamic Require
require(moduleName);

// 12. Buffer Issues
const buf = new Buffer(size);
Enter fullscreen mode Exit fullscreen mode

safe-patterns.js (167 lines) - Contains defensive patterns that should NOT trigger warnings:

// Safe: Validated key access with allowlist
const VALID_KEYS = ['name', 'email', 'age'];
if (VALID_KEYS.includes(key)) {
  return obj[key];
}

// Safe: hasOwnProperty check
if (Object.prototype.hasOwnProperty.call(obj, key)) {
  return obj[key];
}

// Safe: Path validation with startsWith
if (!safePath.startsWith(SAFE_DIR)) throw new Error('Invalid');
fs.readFileSync(safePath);

// Safe: Timing-safe comparison
crypto.timingSafeEqual(bufA, bufB);

// Safe: DOMPurify sanitization
const clean = DOMPurify.sanitize(userContent);
element.innerHTML = clean;
Enter fullscreen mode Exit fullscreen mode

Benchmark Configuration

  • Iterations: 5 runs per test
  • Metrics: Average time, min/max time, issues found, rules triggered
  • Assumption: Run-to-run variance estimated at ≀15%; reported differences (2.83x, 3.8x) exceed this margin

Test 1: Fair Fight (Same 14 Rules)

First, I tested both plugins with only the 14 equivalent rules that exist in both packages. This ensures an apples-to-apples comparison.

Results

Metric secure-coding security Winner
Performance/Issue 24.95ms 25.12ms 🟒 secure-coding
Total Time 723.54ms 527.58ms πŸ”΅ security
Issues Found 29 21 🟒 secure-coding
Detection Rate 138% 100% 🟒 secure-coding

Rule-by-Rule Detection

Rule Category security secure-coding Diff
Timing Attacks 1 5 +4 🟒
Child Process 2 4 +2 🟒
Non-literal Regexp 1 3 +2 🟒
Eval/Code Execution 1 2 +1 🟒
Insufficient Randomness 0 1 +1 🟒
FS Path Traversal 5 5 =
Object Injection 5 5 =
Dynamic Require 2 2 =
Unsafe Regex 2 2 =
Buffer APIs 2 0 -2 πŸ”΅
TOTAL 21 29 +8

Key Finding: With the same rule categories, secure-coding finds 38% more issues while maintaining nearly identical efficiency per issue.


Test 2: Recommended Presets

Next, I tested each plugin's recommended configurationβ€”the out-of-box experience.

Results

Metric secure-coding security Winner
Performance/Issue 9.95ms 28.16ms 🟒 secure-coding
Total Time 795.99ms 591.41ms πŸ”΅ security
Issues Found 80 21 🟒 secure-coding
Rules Triggered 30 10 🟒 secure-coding
Total Rules 89 14 🟒 secure-coding

Detection Breakdown

secure-coding rules triggered on vulnerable.js:

β€’ no-unvalidated-user-input: 8 issues
β€’ detect-non-literal-fs-filename: 5 issues
β€’ detect-object-injection: 5 issues
β€’ no-timing-attack: 5 issues
β€’ detect-child-process: 4 issues
β€’ database-injection: 4 issues
β€’ no-unsafe-deserialization: 4 issues
β€’ no-sql-injection: 3 issues
β€’ detect-non-literal-regexp: 3 issues
β€’ no-hardcoded-credentials: 2 issues
β€’ detect-eval-with-expression: 2 issues
β€’ no-weak-crypto: 2 issues
... and 18 more categories
Enter fullscreen mode Exit fullscreen mode

security rules triggered:

β€’ detect-non-literal-fs-filename: 5 issues
β€’ detect-object-injection: 5 issues
β€’ detect-child-process: 2 issues
β€’ detect-unsafe-regex: 2 issues
... and 6 more categories
Enter fullscreen mode Exit fullscreen mode

Test 3: False Positive Analysis

This is where precision matters. I ran both plugins against safe-patterns.jsβ€”a file with only safe, validated code.

Results

Plugin False Positives Precision
secure-coding 0 100%
security 4 84%

The 4 False Positives from eslint-plugin-security

FP #1: Validated key access (line 38)

// Pattern: Allowlist validation before access
const VALID_KEYS = ['name', 'email', 'age'];
function getField(obj, key) {
  if (VALID_KEYS.includes(key)) {
    return obj[key]; // ⚠️ security flags "Generic Object Injection Sink"
  }
}
Enter fullscreen mode Exit fullscreen mode

The developer validated key against an allowlist. This is a safe pattern.

FP #2: hasOwnProperty check (line 45)

// Pattern: Property existence check before access
function safeGet(obj, key) {
  if (Object.prototype.hasOwnProperty.call(obj, key)) {
    return obj[key]; // ⚠️ security flags "Generic Object Injection Sink"
  }
}
Enter fullscreen mode Exit fullscreen mode

hasOwnProperty ensures key exists on the object itself, not the prototype chain.

FP #3: Guard clause with throw (line 153)

// Pattern: Early exit guard clause
const ALLOWED_THEMES = ['light', 'dark', 'system'];
function setTheme(userTheme) {
  if (!ALLOWED_THEMES.includes(userTheme)) {
    throw new Error('Invalid theme');
  }
  config[userTheme] = true; // ⚠️ security flags despite throw guard
}
Enter fullscreen mode Exit fullscreen mode

The throw acts as a guardβ€”execution cannot reach line 153 with an invalid theme.

FP #4: Path validation (line 107)

// Pattern: basename + startsWith validation
function safeReadFile(userFilename) {
  const safeName = path.basename(userFilename);
  const safePath = path.join(SAFE_DIR, safeName);

  if (!safePath.startsWith(SAFE_DIR)) {
    throw new Error('Invalid path');
  }

  return fs.readFileSync(safePath); // ⚠️ security flags "non literal argument"
}
Enter fullscreen mode Exit fullscreen mode

The path is fully validated: basename strips traversal, startsWith confirms the directory.

Why secure-coding Avoids These

We use AST-based validation detection:

Pattern Detection Method
allowlist.includes(key) Check for includes() in enclosing if-statement
hasOwnProperty(key) Check for hasOwnProperty/hasOwn call
Guard clause + throw Detect preceding IfStatement with early exit
startsWith() validation Detect path validation patterns

OWASP Coverage Comparison

Coverage secure-coding security
OWASP Web Top 10 10/10 (100%) ~3/10 (~30%)
OWASP Mobile Top 10 10/10 (100%) 0/10 (0%)
Total 20/20 ~3/20

LLM/AI Message Comparison

Security rules are increasingly consumed by AI coding assistants. Compare the messages:

eslint-plugin-security:

Found child_process.exec() with non Literal first argument
Enter fullscreen mode Exit fullscreen mode

eslint-plugin-secure-coding:

πŸ”’ CWE-78 OWASP:A03-Injection CVSS:9.8 | Command injection detected | CRITICAL
   Fix: Use execFile/spawn with {shell: false} and array args
   πŸ“š https://owasp.org/www-community/attacks/Command_Injection
Enter fullscreen mode Exit fullscreen mode
Feature secure-coding security
CWE ID βœ… ❌
OWASP Category βœ… ❌
CVSS Score βœ… ❌
Fix Instructions βœ… ❌
Documentation Link βœ… ❌

Feature & Documentation Comparison

Beyond detection metrics, here's the full feature comparison:

Feature secure-coding security
Total Rules 89 14
Documentation Comprehensive (per-rule) Basic
Fix Suggestions/Rule 3-6 suggestions 0
CWE References βœ… All rules ❌ None
CVSS Scores βœ… Yes ❌ No
OWASP Mapping βœ… Web + Mobile ❌ None
TypeScript Support βœ… Full ⚠️ Partial
Flat Config Support βœ… Native βœ… Native
Presets minimal, recommended, strict recommended
Last Updated Active Maintenance mode

Final Verdict

Category secure-coding security Winner
Performance/Issue 9.95ms 28.16ms 🟒 secure-coding
Detection 80 issues 21 issues 🟒 secure-coding
False Positives 0 4 🟒 secure-coding
Precision 100% 84% 🟒 secure-coding
Total Rules 89 14 🟒 secure-coding
OWASP Coverage 20/20 ~3/20 🟒 secure-coding
Documentation Comprehensive Basic 🟒 secure-coding
Fix Suggestions 3-6 per rule 0 🟒 secure-coding
LLM Optimization ⭐⭐⭐⭐⭐ ⭐⭐ 🟒 secure-coding

Key Insights

  1. Performance per issue matters β€” secure-coding is 2.83x more efficient per detected issue.

  2. "Speed advantage" = detection gap β€” The incumbent is faster because it misses vulnerabilities.

  3. 0 false positives β€” Every flagged issue is a real vulnerability.

  4. 6x more rules β€” 89 rules vs 14, covering web, mobile, API, and AI security.

  5. Developer experience β€” Every rule includes CWE/OWASP references, CVSS scores, and 3-6 fix suggestions.


Try It Yourself

npm install eslint-plugin-secure-coding --save-dev
Enter fullscreen mode Exit fullscreen mode
// eslint.config.js
import secureCoding from 'eslint-plugin-secure-coding';

export default [secureCoding.configs.recommended];
Enter fullscreen mode Exit fullscreen mode

The benchmark code is open source: benchmark on GitHub


eslint-plugin-secure-coding on npm

Ofri Peretz | GitHub

Top comments (0)