Node.js Security — Helmet, Rate Limiting, and OWASP Top 10
Node.js security best practices: Helmet headers, rate limiting with express-rate-limit, OWASP Top 10 protections, input sanitization, and security headers for Express APIs..
20+ years shipping production JavaScript and front-end systems at scale. Written from production experience, not tutorials.
- ✓Basic Node.js and JavaScript knowledge. Familiarity with npm and Express.js is recommended.
Node.js security involves multiple layers: HTTP security headers set by Helmet (CSP, HSTS, X-Frame-Options, X-Content-Type-Options), rate limiting via express-rate-limit to prevent brute force and DDo
Think of your Node.js app like a house. Helmet is like installing deadbolts and security cameras on every door and window—it locks down common entry points. Rate limiting is like a bouncer at a club who only lets in a certain number of people per minute, so a crowd can't rush the door all at once. The OWASP Top 10 is a list of the most common ways burglars try to break in, like picking locks or climbing through windows. Together, they keep your house safe from the usual tricks.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
A missing Content-Security-Policy header allowed an attacker to inject a script tag into a comment field. The company's customer data was exfiltrated over 72 hours before the breach was detected. Node.js security is not optional — it is a continuous process of configuring headers, limiting request rates, sanitizing inputs, and monitoring dependencies. This article covers the OWASP Top 10 through the lens of Node.js and Express, with immediate, actionable changes you can make today to harden your API.
The OWASP Top 10: Your Threat Model Baseline
Before writing a single line of security middleware, you must internalize the OWASP Top 10. These are the most critical web application security risks, and Node.js apps are not immune. Broken Access Control (#1) and Injection (#3) are particularly relevant. For example, missing rate limiting can enable brute-force attacks (a form of Broken Access Control). Understanding these categories helps you prioritize which Helmet headers to set and which rate-limiting strategies to adopt. Don't treat security as a checklist; treat it as a continuous risk assessment. Each middleware decision should map to one or more OWASP categories. This section establishes the threat model that the rest of the article builds upon.
Helmet: Hardening HTTP Headers
Helmet is a collection of middleware that sets various HTTP headers to secure your Express app. It's not a silver bullet, but it's the first line of defense against common attacks like XSS, clickjacking, and MIME sniffing. By default, Helmet sets 15 headers, including Content-Security-Policy, X-Content-Type-Options, and X-Frame-Options. However, the defaults are conservative. For production, you must customize the CSP to allow only trusted sources. For example, if you load scripts from a CDN, add that domain to the CSP. Helmet also sets Strict-Transport-Security (HSTS) to enforce HTTPS. Without it, users can be downgraded to HTTP. Always use Helmet with the contentSecurityPolicy option explicitly configured.
eval() will be blocked. Test thoroughly in staging before deploying to production.Rate Limiting: Protecting Against Brute Force and DDoS
Rate limiting is your defense against brute-force login attempts, API abuse, and DDoS attacks. The express-rate-limit package is the go-to for Express apps. You should apply rate limiting globally, but also have stricter limits on sensitive endpoints like /login or /api/register. Use a sliding window algorithm to avoid burst traffic. In production, store rate limit counters in Redis so they persist across server restarts and scale horizontally. Without Redis, if your server restarts, all counters reset, allowing an attacker to brute-force immediately after restart. Also, consider returning a Retry-After header so clients know when to retry.
CORS: Controlling Cross-Origin Access
Cross-Origin Resource Sharing (CORS) is not just about allowing requests from other domains; it's a security mechanism. Misconfigured CORS can expose your API to unauthorized origins. Use the cors package and set specific origins, not wildcards. In production, you should have a whitelist of allowed origins. If your API is only consumed by your frontend, restrict to that domain. Also, be careful with credentials: if you set credentials: true, you cannot use a wildcard origin; you must specify exact origins. CORS headers are part of the OWASP Top 10's Broken Access Control category. Always validate the Origin header server-side if you need dynamic origins.
Access-Control-Allow-Origin: * with credentials: true is invalid and will be rejected by browsers.origin: '*' for their API. An attacker hosted a malicious site that made authenticated requests from users' browsers, stealing data. Always whitelist.Input Validation and Sanitization: Preventing Injection
Injection attacks (SQL, NoSQL, command injection) are still prevalent. In Node.js, you must validate and sanitize all user input. Use libraries like joi or express-validator for validation, and DOMPurify for HTML sanitization if you render user content. Never trust req.body, req.query, or req.params directly. For MongoDB, use mongo-sanitize to prevent $where injections. For SQL, use parameterized queries (e.g., with pg or mysql2). Input validation is your last line of defense before data reaches your database. Combine it with Helmet's CSP to mitigate XSS even if validation fails.
$gt to bypass authentication. Input validation that rejected $ in username fields would have stopped it.Secure Session Management and Authentication
Session management is a common source of vulnerabilities. Use secure, HTTP-only cookies with SameSite and Secure flags. For Express, use express-session with a strong secret and a session store like Redis. Avoid storing sensitive data in the session; store only a user ID and fetch data from the database. Implement account lockout after failed login attempts (combine with rate limiting). Use bcrypt for password hashing with a cost factor of at least 12. For JWT, use short expiration times and store them in HTTP-only cookies, not localStorage. Always validate the JWT signature and check for token revocation.
Error Handling and Logging: Don't Leak Internals
Improper error handling can leak stack traces, database schemas, and other sensitive information. In production, never send raw error objects to the client. Use a centralized error handler that returns generic messages and logs the full error server-side. Use a logging library like winston or pino with structured logging. Include request IDs to correlate errors across services. Also, ensure that your error handler doesn't swallow critical errors; monitor for unhandled promise rejections and uncaught exceptions. These can crash your process or leave it in an inconsistent state.
Dependency Security: Auditing and Updating
Your app is only as secure as its dependencies. The Node.js ecosystem has many packages, and vulnerabilities are discovered regularly. Use npm audit or yarn audit to find known vulnerabilities. Integrate tools like Snyk or Dependabot into your CI/CD pipeline to automatically detect and fix vulnerabilities. Pin your dependencies to exact versions (or use lockfiles) to avoid unexpected updates that introduce breaking changes or vulnerabilities. Regularly update your dependencies, but test thoroughly. Also, consider using a tool like socket.dev to check for malicious packages. A single compromised dependency can lead to a supply chain attack.
npm audit in CI and fail the build if high-severity vulnerabilities are found. Use Dependabot for automatic PRs.Security Headers Beyond Helmet: HSTS, CSP, and More
While Helmet covers many headers, you should understand each one. HSTS (Strict-Transport-Security) forces HTTPS for a specified period. CSP (Content-Security-Policy) controls which resources can be loaded. X-Frame-Options prevents clickjacking. X-Content-Type-Options prevents MIME sniffing. Referrer-Policy controls how much referrer info is sent. Feature-Policy (now Permissions-Policy) restricts browser features. In production, you should also set Expect-CT for Certificate Transparency. Use a tool like securityheaders.com to test your headers. Remember that headers are just one layer; they complement other security measures.
Putting It All Together: A Production Security Checklist
Security is not a one-time setup; it's an ongoing process. Here's a checklist for production: 1) Use Helmet with custom CSP. 2) Apply rate limiting with Redis store. 3) Restrict CORS to specific origins. 4) Validate and sanitize all input. 5) Use secure sessions with HTTP-only cookies. 6) Centralize error handling and log securely. 7) Audit dependencies regularly. 8) Set all relevant security headers. 9) Use HTTPS everywhere (HSTS). 10) Implement account lockout and strong password policies. 11) Use environment variables for secrets. 12) Monitor for anomalies (e.g., sudden traffic spikes). This checklist should be part of your deployment pipeline. Automate as much as possible.
Monitoring and Incident Response
Even with all precautions, breaches can happen. You need monitoring to detect anomalies. Use tools like Prometheus and Grafana to monitor request rates, error rates, and latency. Set up alerts for sudden spikes (possible DDoS) or high error rates (possible exploitation). Have an incident response plan: know who to contact, how to isolate affected systems, and how to rotate keys. Log all security-relevant events (login attempts, permission changes) in a separate, immutable log. Use a SIEM system to correlate events. Practice tabletop exercises to ensure your team knows the drill.
Conclusion: Security is a Mindset
Securing a Node.js application is not about installing a few packages. It's about understanding threats, configuring tools correctly, and maintaining vigilance. Helmet, rate limiting, and input validation are foundational, but they must be part of a broader security strategy. Always keep learning: follow OWASP updates, attend security conferences, and conduct regular security reviews. Remember, security is not a feature; it's a property of the entire system. Build it in from the start, and never treat it as an afterthought.
BOLA (Broken Object Level Authorization) — The #1 API Risk
Broken Object Level Authorization (BOLA) is the most common API vulnerability according to OWASP. It occurs when an API endpoint exposes object identifiers (e.g., user IDs, order numbers) without verifying that the requester owns or is authorized to access that object. For example, a GET /api/orders/:orderId endpoint that returns any order regardless of the authenticated user. Mitigation is straightforward: implement ownership-check middleware that compares the authenticated user's ID with the object's owner ID. Never trust client-supplied identifiers alone. Use parameterized queries or ORM scopes to enforce ownership at the database level. BOLA is often missed in CRUD-heavy apps; every read, update, or delete endpoint must validate authorization for the specific resource.
HPP (HTTP Parameter Pollution) Protection with hpp
HTTP Parameter Pollution (HPP) is an attack where an attacker sends multiple parameters with the same name to confuse the server's parameter parsing logic. For example, ?role=user&role=admin might cause Express to interpret the second value as an array or override the first, potentially leading to privilege escalation. The hpp middleware for Express normalizes duplicate parameters by either taking the last value or rejecting the request. It's a lightweight addition that closes a subtle but exploitable gap. Install with npm install hpp and use app.use(hpp()). You can whitelist parameters that are intentionally arrays (e.g., ?tags=node&tags=security) by passing a whitelist option. HPP is especially important when your application uses query parameters for authorization or filtering.
Rate Limiting: Specific Numbers for Login vs. Global Endpoints
Rate limiting is not one-size-fits-all. For global API endpoints, a common starting point is 100 requests per minute per IP. But for sensitive endpoints like login, password reset, or registration, you need stricter limits: 5 attempts per 15 minutes per IP or per user. This prevents brute-force attacks while allowing legitimate retries. Use express-rate-limit with separate instances for different routes. Store rate limit counters in a distributed store like Redis for multi-instance deployments. Also consider sliding windows vs. fixed windows; sliding windows are more accurate but slightly more complex. For login, also implement account lockout after a number of failed attempts (e.g., 10 in 30 minutes) and require CAPTCHA after that.
JWT + Refresh Token Flow: Secure Authentication
Stateless JWTs are convenient but vulnerable if stolen. A secure pattern uses short-lived access tokens (e.g., 15 minutes) paired with long-lived refresh tokens (e.g., 7 days) stored in an HttpOnly, Secure, SameSite=Strict cookie. The refresh token is also stored server-side (hashed) to allow revocation. On token expiry, the client calls a /refresh endpoint that validates the refresh token and issues a new access token. This minimizes the window of exposure for access tokens and allows logout by deleting the refresh token from the server. Never store JWTs in localStorage (XSS vulnerable). Use the Authorization header for access tokens and cookies for refresh tokens.
Dependency Auditing: npm audit and Snyk
Third-party dependencies are a major attack vector. Run npm audit regularly in CI to detect known vulnerabilities. However, npm audit only covers the npm registry and may miss some issues. For deeper coverage, use Snyk (snyk test) which integrates with GitHub and provides fix advice. Both tools should be part of your CI pipeline, failing builds on high-severity vulnerabilities. Additionally, use npm outdated to track outdated packages and consider tools like Dependabot or Renovate for automated updates. For production, use npm ci instead of npm install to ensure deterministic installs and avoid unexpected version changes. Lockfiles (package-lock.json) must be committed.
Rate Limiter Misconfiguration Causes Global API Outage
- Always test rate limiting with the actual proxy configuration in staging.
- Use a unique identifier like user ID or API key instead of IP when possible.
- Exempt critical endpoints (health checks, monitoring) from rate limiting.
- Implement a circuit breaker to detect and alert on sudden 429 spikes.
| File | Command / Code | Purpose |
|---|---|---|
| threat-model.js | const owaspCategories = { | The OWASP Top 10 |
| helmet-setup.js | const express = require('express'); | Helmet |
| rate-limit.js | const rateLimit = require('express-rate-limit'); | Rate Limiting |
| cors-setup.js | const cors = require('cors'); | CORS |
| input-validation.js | const { body, validationResult } = require('express-validator'); | Input Validation and Sanitization |
| session-setup.js | const session = require('express-session'); | Secure Session Management and Authentication |
| error-handler.js | const winston = require('winston'); | Error Handling and Logging |
| audit.sh | npm audit --audit-level=high | Dependency Security |
| custom-headers.js | app.use((req, res, next) => { | Security Headers Beyond Helmet |
| security-checklist.js | const checklist = [ | Putting It All Together |
| monitoring.js | const prometheus = require('prom-client'); | Monitoring and Incident Response |
| mindset.txt | Security is a mindset, not a checklist. | Conclusion |
| ownershipCheck.js | const ownershipCheck = (model, paramName = 'id') => { | BOLA (Broken Object Level Authorization) |
| hppSetup.js | const express = require('express'); | HPP (HTTP Parameter Pollution) Protection with hpp |
| rateLimiters.js | const rateLimit = require('express-rate-limit'); | Rate Limiting |
| jwtRefreshFlow.js | const jwt = require('jsonwebtoken'); | JWT + Refresh Token Flow |
| ci-audit.sh | npm audit --audit-level=high | Dependency Auditing |
Key takeaways
hpp middleware to prevent HTTP Parameter Pollution. Whitelist parameters that are intentionally arrays.Interview Questions on This Topic
What does the Helmet middleware do in an Express app?
Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Written from production experience, not tutorials.
That's Node.js. Mark it forged?
6 min read · try the examples if you haven't