Enforce Security Rules in Node.js with
How to configure eslint-plugin-security to detect insecure patterns in Node.js code, handle false positives, and integrate with CI/CD pipelines.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Overview
eslint-plugin-security is an ESLint plugin that detects insecure patterns in Node.js code. It checks for things like hardcoded credentials, child_process execution with shell injection risks, regex denial-of-service (ReDoS), prototype pollution, and unsafe eval usage. While not exhaustive, it catches the most common security mistakes in Node.js applications.
When to Use
- Node.js applications handling user input or external data
- Express/Fastify APIs that process requests
- CLI tools that execute shell commands
- Any Node.js codebase where security is a priority
- CI/CD pipelines to catch security issues before deployment
When NOT to Use
- Browser-only code — use browser-specific security tools instead
- TypeScript projects — use
@typescript-eslintwith strict rules (covers some of the same ground) - When you need runtime protection — this is static analysis only
- For dependency vulnerability scanning — use
npm auditorsnyk
Solution
Install
npm install --save-dev eslint eslint-plugin-security
# Using yarn
yarn add --dev eslint eslint-plugin-security
# Using pnpm
pnpm add --save-dev eslint eslint-plugin-security
Basic configuration (flat config)
// eslint.config.js
import security from 'eslint-plugin-security';
export default [
{
plugins: {
security,
},
rules: {
'security/detect-object-injection': 'off', // High false positive rate
'security/detect-non-literal-regexp': 'error',
'security/detect-non-literal-fs-filename': 'warn',
'security/detect-unsafe-regex': 'error',
'security/detect-child-process': 'warn',
'security/detect-buffer-noassert': 'error',
'security/detect-pseudoRandomBytes': 'error',
'security/detect-new-buffer': 'error',
'security/detect-eval-with-expression': 'error',
'security/detect-no-callsites': 'off',
},
},
];
Legacy configuration (.eslintrc)
// .eslintrc.json
{
"plugins": ["security"],
"rules": {
"security/detect-non-literal-regexp": "error",
"security/detect-unsafe-regex": "error",
"security/detect-child-process": "warn",
"security/detect-buffer-noassert": "error",
"security/detect-pseudoRandomBytes": "error",
"security/detect-new-buffer": "error",
"security/detect-eval-with-expression": "error",
"security/detect-non-literal-fs-filename": "warn",
"security/detect-object-injection": "off"
}
}
Use recommended preset
// eslint.config.js
import security from 'eslint-plugin-security';
export default [
security.configs.recommended,
{
// Override specific rules from the recommended preset
rules: {
'security/detect-object-injection': 'off',
'security/detect-child-process': 'error',
},
},
];
Common findings and fixes
detect-non-literal-regexp — ReDoS risk
// BAD — user input in regex, ReDoS risk
const userInput = req.body.pattern;
const regex = new RegExp(userInput);
const match = regex.test(someString);
// GOOD — escape user input or use safe regex
const userInput = req.body.pattern;
const escaped = userInput.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(escaped);
// GOOD — use safe-regex to validate
const safeRegex = require('safe-regex');
if (safeRegex(userInput)) {
const regex = new RegExp(userInput);
}
detect-child-process — Shell injection
// BAD — shell injection risk
const { exec } = require('child_process');
exec(`ls ${req.body.directory}`, (err, stdout) => {
console.log(stdout);
});
// GOOD — use execFile with argument array
const { execFile } = require('child_process');
execFile('ls', ['-la', req.body.directory], (err, stdout) => {
console.log(stdout);
});
// GOOD — use spawn with args array
const { spawn } = require('child_process');
const child = spawn('ls', ['-la', req.body.directory]);
child.stdout.on('data', (data) => console.log(data.toString()));
detect-eval-with-expression — Code injection
// BAD — eval with user input
const result = eval(req.body.expression);
// GOOD — use Function constructor with limited scope
const result = new Function('return ' + sanitizedExpression)();
// BEST — avoid eval entirely, use a parser or JSON
const result = JSON.parse(req.body.jsonData);
detect-new-buffer — Deprecated Buffer constructor
// BAD — new Buffer() is deprecated and insecure
const buf = new Buffer(userInput);
// GOOD — use Buffer.from or Buffer.alloc
const buf = Buffer.from(userInput);
const buf2 = Buffer.alloc(1024); // Safe, zero-filled
detect-pseudoRandomBytes — Weak randomness
// BAD — pseudoRandomBytes for security-sensitive operations
const token = crypto.pseudoRandomBytes(32).toString('hex');
// GOOD — use randomBytes for cryptographic operations
const token = crypto.randomBytes(32).toString('hex');
detect-non-literal-fs-filename — Path traversal
// BAD — user input as filename, path traversal risk
const fs = require('fs');
const content = fs.readFileSync(req.body.filename, 'utf8');
// GOOD — validate and sanitize path
const path = require('path');
const allowedDir = path.resolve('./data');
const requestedPath = path.resolve('./data', req.body.filename);
if (!requestedPath.startsWith(allowedDir)) {
throw new Error('Path traversal detected');
}
const content = fs.readFileSync(requestedPath, 'utf8');
Inline suppression
// eslint-disable-next-line security/detect-child-process
const { exec } = require('child_process');
// eslint-disable-next-line security/detect-non-literal-regexp
const regex = new RegExp(userInput);
CI/CD integration
# .github/workflows/eslint-security.yml
name: ESLint Security
on: [push, pull_request]
jobs:
eslint-security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- name: Run ESLint with security plugin
run: npx eslint . --ext .js,.mjs --format json --output-file eslint-report.json || true
- name: Check for security issues
run: |
SECURITY_ISSUES=$(node -e "
const report = require('./eslint-report.json');
const securityRules = report.filter(f =>
f.messages.some(m => m.ruleId && m.ruleId.startsWith('security/'))
);
if (securityRules.length > 0) {
console.error('Found ' + securityRules.length + ' files with security issues');
process.exit(1);
}
")
Pre-commit hook
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: eslint-security
name: ESLint Security
entry: npx eslint --ext .js,.mjs --rule '{"security/detect-eval-with-expression": "error", "security/detect-child-process": "error"}'
language: system
files: \.js$
Package.json script
{
"scripts": {
"lint:security": "eslint . --ext .js,.mjs --rule 'security/detect-eval-with-expression: error, security/detect-child-process: error, security/detect-unsafe-regex: error'",
"lint:security:fix": "eslint . --ext .js,.mjs --fix"
}
}
Variants
Combined with @typescript-eslint
// eslint.config.js
import tseslint from 'typescript-eslint';
import security from 'eslint-plugin-security';
export default [
...tseslint.configs.recommended,
security.configs.recommended,
{
rules: {
// TypeScript covers some of these, disable duplicates
'security/detect-object-injection': 'off',
'security/detect-new-buffer': 'off', // TS catches this
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unsafe-assignment': 'error',
},
},
];
Custom rule configuration per environment
// eslint.config.js
import security from 'eslint-plugin-security';
export default [
{
plugins: { security },
},
{
// Stricter rules for API/server code
files: ['src/api/**/*.js', 'src/server/**/*.js'],
rules: {
'security/detect-child-process': 'error',
'security/detect-non-literal-fs-filename': 'error',
'security/detect-eval-with-expression': 'error',
'security/detect-unsafe-regex': 'error',
},
},
{
// Relaxed rules for tests
files: ['tests/**/*.js'],
rules: {
'security/detect-child-process': 'off',
'security/detect-non-literal-fs-filename': 'off',
'security/detect-eval-with-expression': 'off',
},
},
];
Using with Husky
{
"scripts": {
"lint:security": "eslint . --ext .js,.mjs --rule 'security/detect-eval-with-expression: error'"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint:security"
}
}
}
Best Practices
-
For a deeper guide, see Detect Bugs in Java with SpotBugs Static Analysis.
-
Start with the recommended preset — then customize based on your codebase
-
Disable
detect-object-injection— it has a very high false positive rate in most codebases -
Use
errorfor high-risk rules — eval, child_process, unsafe regex -
Use
warnfor medium-risk rules — non-literal fs filenames -
Combine with
npm audit— eslint-plugin-security checks code, npm audit checks dependencies -
Run in CI/CD — don’t rely on developers running lint locally
-
Review suppressed rules periodically — new patterns may emerge
-
Combine with TypeScript —
@typescript-eslintcatches many issues that overlap
Common Mistakes
- Enabling all rules at
error: some rules likedetect-object-injectionproduce too many false positives. Start with recommended and adjust. - Suppressing without comments:
// eslint-disable-next-linewithout explanation hides the reason. Always document why. - Only running locally: developers may skip linting. Enforce in CI/CD.
- Ignoring test files: test code can have security issues too, though some rules should be relaxed.
- Not combining with dependency scanning: eslint-plugin-security only checks your code, not your dependencies. Use
npm auditorsnykalongside it.
FAQ
What is eslint-plugin-security?
An ESLint plugin that detects insecure patterns in Node.js code — shell injection, eval, weak crypto, ReDoS, deprecated Buffer, and more.
Should I enable detect-object-injection?
Usually no. It flags any bracket notation property access (obj[key]), which is extremely common in JavaScript. The false positive rate is very high for most codebases.
Does eslint-plugin-security work with TypeScript?
Yes, but some rules overlap with @typescript-eslint. Use both, but disable duplicate rules to avoid noise.
How is this different from npm audit?
eslint-plugin-security scans your source code for insecure patterns. npm audit scans your dependencies for known vulnerabilities. Use both for detailed coverage.
Can I use this for browser code?
It’s designed for Node.js. Some rules (like detect-child-process) are Node-specific. For browser code, use ESLint with appropriate browser rulesets.
Related Resources
Find Security Issues in Python Code with Bandit
How to use Bandit to scan Python code for common security vulnerabilities, configure ignore lists, integrate with CI/CD, and interpret results.
RecipeStrict TypeScript ESLint Configuration for Production
How to configure typescript-eslint with strict rules for production TypeScript projects, handle type-aware linting, and integrate with CI/CD.
RecipeShare Workflow Logic with GitHub Actions Reusable Workflows
How to create and consume reusable workflows in GitHub Actions, covering inputs, secrets, conditional jobs, matrix strategy, and organization-wide sharing.
RecipeDetect Bugs in Java with SpotBugs Static Analysis
How to configure SpotBugs for Maven and Gradle, interpret bug patterns, suppress false positives, and integrate with CI/CD pipelines.