| name | nodejs-security-guide |
| description | Reference knowledge base for the nodejs-security-audit agent. Loaded by that agent on its first iteration when the host has not already injected it; not intended for direct invocation. |
Node.js / TypeScript Security: Comprehensive Guide to Identifying and Patching Vulnerabilities
Table of Contents
- Introduction
- Security Tools and Analysis
- Common Vulnerabilities
- Cryptography Best Practices
- Dependency Security
- Security Checklist
Introduction
Node.js is widely deployed in production systems handling sensitive data. While
JavaScript's sandboxed execution prevents many low-level memory exploits, the
ecosystem has unique vulnerabilities: prototype pollution, unhandled Promise
rejections, and a rich dependency tree that expands the attack surface.
Key Security Principles
- Validate all external input at system boundaries
- Never trust data from
req.body, req.params, req.query, or req.headers
- Use parameterized queries for all database access
- Use
crypto.randomBytes() for all security-sensitive random values
- Pin dependencies and run
npm audit in CI
- Never expose internal error details or stack traces to clients
Security Tools and Analysis
1. npm audit — Built-in Vulnerability Scanner
npm audit
npm audit fix
npm audit --json
2. Snyk — Deep Dependency Analysis
npm install -g snyk
snyk test
snyk monitor
3. ESLint Security Plugins
npm install --save-dev eslint-plugin-security eslint-plugin-no-unsanitized
module.exports = {
plugins: ['security', 'no-unsanitized'],
extends: ['plugin:security/recommended'],
};
Key rules:
security/detect-non-literal-regexp — dynamic regex from user input
security/detect-child-process — exec/spawn with user input
security/detect-object-injection — obj[userKey] prototype pollution
security/detect-possible-timing-attacks — non-constant-time comparisons
4. SAST Tools
Semgrep for custom rules:
pip install semgrep
semgrep --config p/nodejs-security .
CodeQL (GitHub):
- uses: github/codeql-action/analyze@v3
with:
languages: javascript
Common Vulnerabilities
1. Injection Attacks
Command Injection
Vulnerable:
const { exec } = require('child_process');
exec('ls ' + req.query.dir, callback);
Secure:
const { execFile } = require('child_process');
execFile('/bin/ls', ['-la', sanitizedDir], callback);
const { spawn } = require('child_process');
const ls = spawn('/bin/ls', ['-la', sanitizedDir]);
Prevention:
- Use
execFile or spawn with explicit argument arrays
- Never pass user input to
exec(), eval(), or new Function(str)
- Allowlist permitted values when possible
SQL Injection
Vulnerable:
const query = `SELECT * FROM users WHERE username='${username}'`;
db.query(query);
Secure:
db.query('SELECT * FROM users WHERE username = $1', [username]);
db.execute('SELECT * FROM users WHERE username = ?', [username]);
prisma.user.findUnique({ where: { username } });
knex('users').where({ username });
eval() and Dynamic Code Execution
eval(userCode);
new Function(userCode)();
setTimeout(userCode, 1000);
const vm = require('vm2');
const sandbox = new vm.VM({ sandbox: {} });
sandbox.run(userCode);
2. Prototype Pollution
Vulnerability: Allows attackers to modify Object.prototype, affecting all objects.
Vulnerable:
Object.assign(target, req.body);
_.merge(config, userInput);
const obj = {};
obj[req.body.key] = req.body.value;
Secure:
const ALLOWED_KEYS = new Set(['name', 'email', 'age']);
for (const [key, value] of Object.entries(userInput)) {
if (ALLOWED_KEYS.has(key)) obj[key] = value;
}
const safeDict = Object.create(null);
const schema = z.object({ name: z.string(), email: z.string().email() });
const safe = schema.parse(req.body);
function isSafeKey(key) {
return key !== '__proto__' && key !== 'constructor' && key !== 'prototype';
}
Detection:
const testObj = {};
console.log(testObj.polluted);
3. Path Traversal
Vulnerable:
app.get('/file', (req, res) => {
const filePath = '/var/www/files/' + req.query.name;
res.sendFile(filePath);
});
Secure (Node 18+):
import path from 'path';
app.get('/file', (req, res) => {
const BASE = path.resolve('/var/www/files');
const requested = path.resolve(BASE, req.query.name);
if (!requested.startsWith(BASE + path.sep)) {
return res.status(403).json({ error: 'Access denied' });
}
res.sendFile(requested);
});
Or use express.static with root option (handles this automatically):
app.use('/files', express.static('/var/www/files'));
4. Cross-Site Scripting (XSS)
Vulnerability: User content rendered as HTML/JS in browsers.
Vulnerable (Server-side rendering):
res.send(`<div>${req.query.search}</div>`);
<div dangerouslySetInnerHTML={{ __html: userContent }} />
Secure:
import nunjucks from 'nunjucks';
nunjucks.configure({ autoescape: true });
res.send(nunjucks.render('template.html', { search: req.query.search }));
<div>{userContent}</div>
element.textContent = userInput;
element.innerHTML = userInput;
Content Security Policy:
import helmet from 'helmet';
app.use(helmet());
5. SSRF (Server-Side Request Forgery)
Vulnerability: Server makes HTTP requests to attacker-controlled URLs.
Vulnerable:
app.post('/fetch', async (req, res) => {
const response = await fetch(req.body.url);
res.json(await response.json());
});
Secure:
const ALLOWED_HOSTS = new Set(['api.example.com', 'cdn.example.com']);
app.post('/fetch', async (req, res) => {
const url = new URL(req.body.url);
if (!ALLOWED_HOSTS.has(url.hostname)) {
return res.status(400).json({ error: 'Host not allowed' });
}
const response = await fetch(url.toString());
res.json(await response.json());
});
Cryptography Best Practices
1. Secure Random Values
NEVER use Math.random() for security:
const token = Math.random().toString(36);
import { randomBytes } from 'crypto';
const token = randomBytes(32).toString('hex');
const otp = String(Math.floor(parseInt(randomBytes(3).toString('hex'), 16) % 1000000)).padStart(6, '0');
2. Hashing
AVOID for password storage or security-sensitive checksums:
- MD5 — broken, collision attacks known
- SHA-1 — deprecated, collision attacks demonstrated
USE:
import { createHash, scrypt, randomBytes } from 'crypto';
const hash = createHash('sha256').update(data).digest('hex');
import bcrypt from 'bcrypt';
const passwordHash = await bcrypt.hash(password, 12);
const isValid = await bcrypt.compare(password, passwordHash);
const salt = randomBytes(16);
const derivedKey = await new Promise((resolve, reject) =>
scrypt(password, salt, 64, (err, key) => err ? reject(err) : resolve(key))
);
3. Encryption
AES-GCM (authenticated encryption):
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
function encrypt(plaintext, key) {
const iv = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', key, iv);
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
const tag = cipher.getAuthTag();
return { iv, encrypted, tag };
}
function decrypt({ iv, encrypted, tag }, key) {
const decipher = createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(tag);
return Buffer.concat([decipher.update(encrypted), decipher.final()]);
}
AVOID deprecated APIs:
crypto.createCipher('aes256', password);
4. TLS Configuration
import https from 'https';
import fs from 'fs';
const server = https.createServer({
key: fs.readFileSync('server.key'),
cert: fs.readFileSync('server.crt'),
minVersion: 'TLSv1.2',
});
const agent = new https.Agent({ rejectUnauthorized: true });
5. JWT Security
import jwt from 'jsonwebtoken';
jwt.verify(token, secret, { algorithms: ['HS256'] });
jwt.sign({ userId }, secret, { expiresIn: '1h', algorithm: 'HS256' });
jwt.sign(payload, privateKey, { algorithm: 'RS256' });
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
Dependency Security
1. Lock Files and Version Pinning
{
"dependencies": {
"express": "4.18.2",
"jsonwebtoken": "^9.0.0"
}
}
2. CI Integration
name: Security Audit
on: [push, pull_request]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm audit --audit-level=high
- run: npx snyk test || true
3. Supply Chain Risks
- Check
postinstall scripts in transitive dependencies
- Use
npm pack to inspect what gets installed
- Use private registry mirrors for critical packages
- Review new dependencies before merging
ReDoS (Regular Expression Denial of Service)
Vulnerable Patterns
Catastrophic backtracking occurs with nested quantifiers on user-controlled input:
/^(a+)+$/.test(userInput)
/(a|aa)+$/.test(userInput)
/(\w+\s?)*$/.test(userInput)
Detection
Use a tool like safe-regex or redos-detector:
npm install --save-dev safe-regex
Fix
- Simplify regex to avoid nested quantifiers
- Use atomic groups or possessive quantifiers (Node 16+)
- Validate input length before applying complex regex
- Set a timeout using
AbortSignal:
import { setTimeout as setTimeoutPromise } from 'timers/promises';
async function safeMatch(regex, input) {
if (input.length > 1000) throw new Error('Input too long');
return regex.test(input);
}
Security Checklist
Input Handling
Cryptography
Injection
Infrastructure
Error Handling
References
Document Version: 1.0
Last Updated: May 2026
Author: Compiled from OWASP resources, Node.js docs, and community security research