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);
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;
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
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
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"
}
}
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"
}
}
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
}
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"
}
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
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
| 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
Performance per issue matters β
secure-codingis 2.83x more efficient per detected issue."Speed advantage" = detection gap β The incumbent is faster because it misses vulnerabilities.
0 false positives β Every flagged issue is a real vulnerability.
6x more rules β 89 rules vs 14, covering web, mobile, API, and AI security.
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
// eslint.config.js
import secureCoding from 'eslint-plugin-secure-coding';
export default [secureCoding.configs.recommended];
The benchmark code is open source: benchmark on GitHub
Top comments (0)