1. Introduction đ
Building a secure Node.js application requires more than just writing functional code â it demands deliberate attention to authentication, data handling, dependencies, and infrastructure. This tutorial covers the essential security concepts every Node.js developer must understand, from fundamentals to production monitoring.
Warning
2. Node.js Security Fundamentals đ§ą
Node.js security rests on a few core principles that apply across every layer of an application.
- Defense in depth â layer multiple protections so a single failure doesn't compromise the system.
- Least privilege â processes, users, and services should only have the access they strictly need.
- Never trust user input â validate and sanitize everything coming from outside your system.
- Keep dependencies updated â most real-world breaches exploit known, already-patched vulnerabilities.
Information
3. Common Security Threats â ī¸
| Threat | Impact |
|---|---|
| SQL / NoSQL Injection | Unauthorized data access or modification |
| Cross-Site Scripting (XSS) | Malicious scripts executed in users' browsers |
| Cross-Site Request Forgery (CSRF) | Unauthorized actions performed as an authenticated user |
| Broken Authentication | Account takeover, session hijacking |
| Insecure Dependencies | Remote code execution via vulnerable packages |
| Denial of Service (DoS) | Service unavailability |
Reference
4. Environment Variable Security đ
Environment variables often hold sensitive data like database credentials and API keys. They MUST be handled carefully.
.env (never commit this file)
DATABASE_URL=postgres://user:pass@localhost:5432/db
JWT_SECRET=super-long-random-secret
API_KEY=sk_live_xxx.gitignore
.env
.env.local
.env.*.localDanger
5. Secrets Management đī¸
For production systems, plain .env files are often insufficient. Dedicated secrets managers provide encryption, rotation, and access auditing.
- Cloud-native options: AWS Secrets Manager, Google Secret Manager, Azure Key Vault.
- Self-hosted options: HashiCorp Vault, Doppler.
- Rotation: secrets should be rotated periodically and immediately after any suspected leak.
Best Practice
6. Authentication đĒĒ
Authentication verifies who a user is. Common strategies in Node.js include credential-based login, OAuth, and passwordless flows.
src/auth/login.ts
import bcrypt from "bcrypt";
async function login(email: string, password: string) {
const user = await findUserByEmail(email);
if (!user) throw new Error("Invalid credentials");
const isValid = await bcrypt.compare(password, user.passwordHash);
if (!isValid) throw new Error("Invalid credentials");
return generateSessionToken(user.id);
}Tip
7. Authorization đ
Authorization determines what an authenticated user is allowed to do. It should be enforced on the server for every protected action.
src/middleware/authorize.ts
function authorize(requiredRole: string) {
return (req: AuthRequest, res: Response, next: NextFunction) => {
if (req.user?.role !== requiredRole) {
return res.status(403).json({ error: "Forbidden" });
}
next();
};
}
router.delete("/users/:id", authorize("admin"), deleteUser);Caution
8. Password Hashing đ¨
Passwords must never be stored in plain text. Use a slow, adaptive hashing algorithm designed to resist brute-force attacks.
src/auth/hash.ts
import bcrypt from "bcrypt";
async function hashPassword(plain: string): Promise<string> {
const saltRounds = 12;
return bcrypt.hash(plain, saltRounds);
}| Algorithm | Notes |
|---|---|
| bcrypt | Well-established, widely supported, good default choice. |
| argon2 | Winner of the Password Hashing Competition; strongest option available today. |
| scrypt | Memory-hard, built into Node's crypto module. |
Error
9. JWT Security đĢ
JSON Web Tokens are commonly used for stateless authentication, but they come with sharp edges if misconfigured.
src/auth/jwt.ts
import jwt from "jsonwebtoken";
function generateToken(userId: string): string {
return jwt.sign({ sub: userId }, process.env.JWT_SECRET as string, {
algorithm: "HS256",
expiresIn: "15m",
});
}
function verifyToken(token: string) {
return jwt.verify(token, process.env.JWT_SECRET as string, {
algorithms: ["HS256"],
});
}- Always set a short expiresIn and use refresh tokens for long-lived sessions.
- Explicitly whitelist allowed algorithms during verification.
- Store JWTs in HttpOnly, Secure cookies â not in localStorage.
Danger
10. Session Security đĒ
For traditional session-based authentication, cookies must be configured with strict security flags.
src/session.ts
app.use(session({
secret: process.env.SESSION_SECRET as string,
cookie: {
httpOnly: true,
secure: true,
sameSite: "strict",
maxAge: 1000 * 60 * 30,
},
resave: false,
saveUninitialized: false,
}));Note
11. Input Validation â
All incoming data â request bodies, query params, headers â must be validated against a strict schema before use.
src/validation/user.ts
import { z } from "zod";
const createUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().positive().optional(),
});
function validateCreateUser(data: unknown) {
return createUserSchema.parse(data);
}Best Practice
12. Output Sanitization đ§ŧ
Data must also be sanitized on the way out, especially when rendering user-generated content in HTML.
src/sanitize.ts
import DOMPurify from "isomorphic-dompurify";
function sanitizeHtml(dirty: string): string {
return DOMPurify.sanitize(dirty, { ALLOWED_TAGS: ["b", "i", "em", "strong"] });
}Tip
13. SQL Injection Prevention đ
SQL injection occurs when untrusted input is concatenated directly into a query string. The fix is parameterized queries.
src/db/users.ts
// â Vulnerable
const query = `SELECT * FROM users WHERE email = '${email}'`;
// â
Safe â parameterized query
const result = await pool.query(
"SELECT * FROM users WHERE email = $1",
[email]
);Important
14. NoSQL Injection Prevention đ
NoSQL databases like MongoDB are vulnerable to injection when user input is passed directly as a query operator.
src/db/mongo.ts
// â Vulnerable â attacker sends { "$ne": null } as password
const user = await User.findOne({ email, password: req.body.password });
// â
Safe â cast and validate types explicitly first
const password = String(req.body.password);
const user = await User.findOne({ email, password });Warning
15. Cross-Site Scripting (XSS) đ¸ī¸
XSS lets attackers inject malicious scripts into pages viewed by other users, often by exploiting unescaped output.
- Escape all user-generated content before rendering it in HTML.
- Use templating engines that auto-escape by default (most modern frameworks do).
- Set a strict Content-Security-Policy header (see Section 18).
- Avoid dangerouslySetInnerHTML or equivalent unless content is sanitized first.
Example
16. Cross-Site Request Forgery (CSRF) đ
CSRF tricks an authenticated user's browser into submitting an unwanted request to your application.
src/middleware/csrf.ts
import csrf from "csurf";
const csrfProtection = csrf({ cookie: { httpOnly: true, sameSite: "strict" } });
app.post("/transfer", csrfProtection, (req, res) => {
// req.body._csrf is validated automatically
processTransfer(req.body);
});Tip
17. CORS đ
Cross-Origin Resource Sharing controls which external origins may call your API from a browser. Misconfigured CORS can expose private endpoints.
src/cors.ts
import cors from "cors";
app.use(cors({
origin: ["https://app.example.com"],
credentials: true,
methods: ["GET", "POST", "PUT", "DELETE"],
}));Danger
18. Security Headers đĄī¸
HTTP security headers instruct the browser to enable additional protections against common attacks.
src/index.ts
import helmet from "helmet";
app.use(helmet({
contentSecurityPolicy: {
directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'"] },
},
hsts: { maxAge: 31536000, includeSubDomains: true },
}));| Header | Protects Against |
|---|---|
| Content-Security-Policy | XSS, data injection |
| Strict-Transport-Security | Protocol downgrade attacks |
| X-Content-Type-Options | MIME-sniffing |
| X-Frame-Options | Clickjacking |
19. HTTPS & TLS đ
All production traffic MUST be encrypted in transit using TLS. Never transmit credentials or tokens over plain HTTP.
src/server.ts
import https from "node:https";
import fs from "node:fs";
const options = {
key: fs.readFileSync("privkey.pem"),
cert: fs.readFileSync("fullchain.pem"),
};
https.createServer(options, app).listen(443);Note
20. Rate Limiting đĻ
Rate limiting protects against brute-force attacks, credential stuffing, and denial-of-service attempts.
src/middleware/rateLimit.ts
import rateLimit from "express-rate-limit";
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: "Too many login attempts, please try again later.",
});
app.post("/login", loginLimiter, loginHandler);Tip
21. File Upload Security đ¤
File uploads are a common attack vector if file type, size, and content aren't strictly controlled.
- Validate file type by inspecting content (magic bytes), not just the file extension.
- Enforce a strict maximum file size.
- Store uploads outside the web root, or in isolated object storage (e.g. S3).
- Rename uploaded files to random, generated names to prevent path traversal.
- Scan uploads for malware where feasible.
Caution
22. Dependency Security đĻ
Third-party packages are a leading source of real-world vulnerabilities in Node.js applications.
Terminal
npm audit
npm audit fix
npm outdated- Run npm audit regularly and in CI pipelines.
- Use tools like Snyk or Dependabot for automated vulnerability scanning.
- Pin dependency versions using a lockfile (package-lock.json).
- Avoid installing packages with very few downloads or no recent maintenance.
23. Secure Logging đ
Logs are essential for debugging and monitoring, but they can also leak sensitive data if not handled carefully.
src/logger.ts
import pino from "pino";
const logger = pino({
redact: ["req.headers.authorization", "*.password", "*.token"],
});
logger.info({ userId: user.id }, "User logged in");Important
24. Secure Configuration âī¸
Sensible defaults reduce your attack surface even before any code runs.
- Disable the X-Powered-By: Express header to avoid revealing framework details.
- Run the Node process as a non-root user in containers.
- Set resource limits (memory, CPU) to reduce the blast radius of a compromised process.
- Disable unused HTTP methods and routes.
src/index.ts
app.disable("x-powered-by");25. Security Monitoring đ
Detecting an attack in progress is as important as preventing it. Monitoring closes the gap between compromise and response.
26. Best Practices â
- Validate and sanitize all input, regardless of its source.
- Use parameterized queries and trusted ORMs to prevent injection attacks.
- Hash passwords with bcrypt or argon2 â never store them in plain text.
- Apply the principle of least privilege to users, tokens, and service accounts.
- Keep dependencies patched and monitor for known vulnerabilities continuously.
- Enforce HTTPS everywhere and set strict security headers.
27. Common Mistakes â ī¸
| Mistake | Consequence |
|---|---|
| Storing secrets in source code | Credentials leaked via Git history |
| Trusting client-side validation only | Attackers bypass checks entirely via direct API calls |
| Using outdated dependencies | Exposure to known, publicly documented exploits |
| Verbose error messages in production | Leaks stack traces and internal architecture details |
| Overly permissive CORS settings | Unauthorized cross-origin access to the API |
28. Frequently Asked Questions â
Question
Answer
Question
Answer
Question
Answer
Question
Answer
29. Summary đ
Security in Node.js spans every layer of an application: authentication, authorization, input handling, network configuration, and ongoing monitoring. No single control is sufficient on its own â real protection comes from combining many layers of defense.
- Hash passwords properly and secure sessions/tokens with strict cookie flags.
- Validate and sanitize all input; use parameterized queries everywhere.
- Set strong security headers, enforce HTTPS, and configure CORS carefully.
- Keep dependencies updated and audit them continuously.
- Monitor actively and have an incident response plan ready.