Overview
Security is critical for Node.js applications. This guide covers the Node.js security model, best practices, and the experimental Permission Model for restricting runtime capabilities.
Node.js trusts the code it is asked to run. The Permission Model implements a “seat belt” approach to prevent trusted code from unintentional mistakes, but it does not protect against malicious code.
Reporting Security Vulnerabilities
How to Report
Report security bugs via HackerOne :
Acknowledgement : Within 5 days
Detailed Response : Within 10 days
Bug Bounty : Available through HackerOne
If you don’t receive acknowledgement within 6 business days, escalate to [email protected]
Disclosure Policy
Report received
Security team assigns a primary handler who coordinates the fix and release.
Validation
Issue is validated against all supported Node.js versions. Code is audited for similar problems.
CVE requested
An embargo date is chosen and a CVE is requested.
Release
On embargo date (typically 72 hours after CVE), fixes are released and advisory published.
Node.js Threat Model
Trusted Elements
Node.js trusts:
The operating system and its configuration
The code it runs (including all dependencies)
The developers and infrastructure
File system when requiring modules
Inspector protocol connections
Untrusted Elements
Node.js does NOT trust:
Data from inbound network connections
Data from outbound network connections (except payload length)
File content opened for reading/writing
Console data consumers
What Constitutes a Vulnerability
A vulnerability exists if untrusted input can cause:
Disclosure or loss of confidentiality/integrity of protected data
Unavailability of the runtime
Unbounded performance degradation
Examples of Vulnerabilities
Improper Certificate Validation (CWE-295)
Bugs in TLS/SSL certificate validation that allow crafted certificates to bypass security checks.
HTTP Request Smuggling (CWE-444)
Parsing bugs in HTTP header handling that enable request smuggling attacks.
Missing Cryptographic Step (CWE-325)
Bugs allowing attackers to recover encrypted data without the decryption key.
External Control of Configuration (CWE-15)
Undocumented automatic configuration file loading that affects data confidentiality.
Non-Vulnerabilities
Malicious Third-Party Modules
Code is trusted. Scenarios requiring malicious modules are not vulnerabilities.
Applications must sanitize user input. This is not a Node.js vulnerability.
Node.js trusts the file system. Accessing files from any accessible path is not a vulnerability.
Permission Model
The Permission Model restricts access to specific resources during execution.
The Permission Model has been stable since Node.js v23.5.0 and v22.13.0.
Enabling Permissions
Start Node.js with the --permission flag:
node --permission index.js
This restricts:
File system access (fs module)
Network access
Child processes
Worker threads
Native addons
WASI
Inspector protocol
Granting File System Access
Allow specific file system operations:
Read Access
Write Access
Full Access
node --permission --allow-fs-read=/tmp/ --allow-fs-read=/home/.gitignore index.js
Wildcard Support
Use wildcards to grant broader access:
# Allow read access to everything matching pattern
node --permission --allow-fs-read=/home/test * index.js
# Matches: /home/test/file1, /home/test2, etc.
Characters after * are ignored. /home/*.js works like /home/*
Other Permission Flags
Allow spawning child processes
Allow creating worker threads
Runtime Permission Checks
Check permissions programmatically:
// Check general permission
process . permission . has ( 'fs.write' ); // true or false
// Check specific path
process . permission . has ( 'fs.write' , '/home/user/protected' ); // true or false
// Check read permission
process . permission . has ( 'fs.read' ); // true
process . permission . has ( 'fs.read' , '/etc/passwd' ); // false
Configuration File
Define permissions in node.config.json:
{
"permission" : {
"allow-fs-read" : [ "./foo" ],
"allow-fs-write" : [ "./bar" ],
"allow-child-process" : true ,
"allow-worker" : true ,
"allow-net" : true ,
"allow-addons" : false
}
}
Run with:
node --experimental-default-config-file app.js
Permission Model Constraints
Be aware of these limitations:
Permissions don’t inherit to worker threads
Some flags like --env-file execute before permission model initialization
OpenSSL engines cannot be requested at runtime
Using existing file descriptors bypasses permission checks
Symbolic links are followed even outside granted paths
Security Best Practices
Always validate and sanitize user input:
import validator from 'validator' ;
// Validate email
if ( ! validator . isEmail ( userInput )) {
throw new Error ( 'Invalid email' );
}
// Sanitize HTML
const clean = validator . escape ( userInput );
// Validate URLs
if ( ! validator . isURL ( url , { protocols: [ 'https' ] })) {
throw new Error ( 'Invalid URL' );
}
Avoid Command Injection
import { exec } from 'node:child_process' ;
// DANGEROUS: User input in command
exec ( `ls ${ userInput } ` );
Prevent Path Traversal
import path from 'node:path' ;
import fs from 'node:fs/promises' ;
const UPLOAD_DIR = '/var/uploads' ;
async function serveFile ( filename ) {
// Prevent path traversal
const safePath = path . resolve ( UPLOAD_DIR , filename );
if ( ! safePath . startsWith ( UPLOAD_DIR )) {
throw new Error ( 'Access denied' );
}
return fs . readFile ( safePath );
}
Use Prepared Statements
Prevent SQL injection:
import { DatabaseSync } from 'node:sqlite' ;
const db = new DatabaseSync ( ':memory:' );
// SAFE: Parameterized query
const stmt = db . prepare ( 'SELECT * FROM users WHERE id = ?' );
const user = stmt . get ( userId );
// DANGEROUS: String concatenation
// const user = db.exec(`SELECT * FROM users WHERE id = ${userId}`);
import express from 'express' ;
import helmet from 'helmet' ;
const app = express ();
// Use Helmet for security headers
app . use ( helmet ());
// Or set manually
app . use (( req , res , next ) => {
res . setHeader ( 'X-Content-Type-Options' , 'nosniff' );
res . setHeader ( 'X-Frame-Options' , 'DENY' );
res . setHeader ( 'X-XSS-Protection' , '1; mode=block' );
res . setHeader ( 'Strict-Transport-Security' , 'max-age=31536000' );
next ();
});
Environment Variables
Protect sensitive configuration:
// Load environment variables
import 'dotenv/config' ;
// Never log secrets
const apiKey = process . env . API_KEY ;
if ( ! apiKey ) {
throw new Error ( 'API_KEY not configured' );
}
// Use process.env only once
const config = {
apiKey: process . env . API_KEY ,
dbUrl: process . env . DATABASE_URL
};
// Don't expose in error messages
try {
await api . call ( config . apiKey );
} catch ( err ) {
// Good: Generic message
throw new Error ( 'API call failed' );
// Bad: Exposes secret
// throw new Error(`API call failed with key ${config.apiKey}`);
}
Dependency Security
Keep dependencies updated:
# Check for vulnerabilities
npm audit
# Fix automatically if possible
npm audit fix
# Update outdated packages
npm outdated
npm update
# Use exact versions in production
npm install --save-exact package-name
Rate Limiting
Prevent abuse:
import rateLimit from 'express-rate-limit' ;
const limiter = rateLimit ({
windowMs: 15 * 60 * 1000 , // 15 minutes
max: 100 , // Limit each IP to 100 requests per window
message: 'Too many requests from this IP'
});
app . use ( '/api/' , limiter );
Crypto Best Practices
import crypto from 'node:crypto' ;
// Generate secure random values
const token = crypto . randomBytes ( 32 ). toString ( 'hex' );
// Hash passwords with scrypt
const password = 'user-password' ;
const salt = crypto . randomBytes ( 16 );
crypto . scrypt ( password , salt , 64 , ( err , derivedKey ) => {
if ( err ) throw err ;
// Store derivedKey and salt
});
// Use timing-safe comparison
function verifyToken ( userToken , validToken ) {
const userBuf = Buffer . from ( userToken );
const validBuf = Buffer . from ( validToken );
if ( userBuf . length !== validBuf . length ) {
return false ;
}
return crypto . timingSafeEqual ( userBuf , validBuf );
}
Common Vulnerabilities
Cross-Site Scripting (XSS)
import createDOMPurify from 'dompurify' ;
import { JSDOM } from 'jsdom' ;
const window = new JSDOM ( '' ). window ;
const DOMPurify = createDOMPurify ( window );
// Sanitize HTML
const dirty = '<img src=x onerror=alert(1)>' ;
const clean = DOMPurify . sanitize ( dirty );
Cross-Site Request Forgery (CSRF)
import csrf from 'csurf' ;
const csrfProtection = csrf ({ cookie: true });
app . get ( '/form' , csrfProtection , ( req , res ) => {
res . render ( 'form' , { csrfToken: req . csrfToken () });
});
app . post ( '/process' , csrfProtection , ( req , res ) => {
// Protected against CSRF
});
Server-Side Request Forgery (SSRF)
import { URL } from 'node:url' ;
const ALLOWED_HOSTS = [ 'api.example.com' , 'cdn.example.com' ];
function validateUrl ( urlString ) {
const url = new URL ( urlString );
// Only allow HTTPS
if ( url . protocol !== 'https:' ) {
throw new Error ( 'Only HTTPS allowed' );
}
// Only allow specific hosts
if ( ! ALLOWED_HOSTS . includes ( url . hostname )) {
throw new Error ( 'Host not allowed' );
}
// Prevent private IPs
const ip = url . hostname ;
if ( ip . startsWith ( '127.' ) || ip . startsWith ( '10.' ) ||
ip . startsWith ( '192.168.' ) || ip . startsWith ( '172.' )) {
throw new Error ( 'Private IPs not allowed' );
}
return url ;
}
Security Checklist
Keep Node.js Updated
Use latest LTS version and apply security patches promptly.
Validate All Input
Never trust user input. Validate, sanitize, and escape.
Use HTTPS
Always use TLS for network communication.
Implement Authentication
Use proven authentication methods (OAuth, JWT with proper validation).
Apply Authorization
Check permissions for every protected resource.
Audit Dependencies
Regularly run npm audit and update packages.
Use Security Headers
Implement CSP, HSTS, X-Frame-Options, etc.
Enable Logging
Log security events but never log secrets.
Implement Rate Limiting
Protect against brute force and DoS attacks.
Review Code
Conduct security reviews before deployment.
Resources
Next Steps
Building from Source Build Node.js securely
Performance Secure performance optimization