Skip to main content

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

1

Report received

Security team assigns a primary handler who coordinates the fix and release.
2

Validation

Issue is validated against all supported Node.js versions. Code is audited for similar problems.
3

CVE requested

An embargo date is chosen and a CVE is requested.
4

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

Bugs in TLS/SSL certificate validation that allow crafted certificates to bypass security checks.
Parsing bugs in HTTP header handling that enable request smuggling attacks.
Bugs allowing attackers to recover encrypted data without the decryption key.
Undocumented automatic configuration file loading that affects data confidentiality.

Non-Vulnerabilities

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:
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-child-process
flag
Allow spawning child processes
--allow-worker
flag
Allow creating worker threads
--allow-net
flag
Allow network access
--allow-addons
flag
Allow native addons
--allow-wasi
flag
Allow WASI access

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

Input Validation

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}`);

Secure HTTP Headers

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

1

Keep Node.js Updated

Use latest LTS version and apply security patches promptly.
2

Validate All Input

Never trust user input. Validate, sanitize, and escape.
3

Use HTTPS

Always use TLS for network communication.
4

Implement Authentication

Use proven authentication methods (OAuth, JWT with proper validation).
5

Apply Authorization

Check permissions for every protected resource.
6

Audit Dependencies

Regularly run npm audit and update packages.
7

Use Security Headers

Implement CSP, HSTS, X-Frame-Options, etc.
8

Enable Logging

Log security events but never log secrets.
9

Implement Rate Limiting

Protect against brute force and DoS attacks.
10

Review Code

Conduct security reviews before deployment.

Resources

Next Steps

Building from Source

Build Node.js securely

Performance

Secure performance optimization