Home JavaScript Node.js Security — Helmet, Rate Limiting, and OWASP Top 10
Advanced 6 min · 2026-07-12

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..

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Written from production experience, not tutorials.

Follow
Production
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 15-20 minutes
  • Basic Node.js and JavaScript knowledge. Familiarity with npm and Express.js is recommended.
 ● Production Incident
Quick Answer

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

✦ Definition~90s read
What is Node.js Security?

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 DDoS attacks, input validation and sanitization to prevent injection attacks, and CSRF protection. The OWASP Top 10 vulnerabilities that apply to Node.js include Broken Access Control, Injection, Security Misconfiguration (missing headers), Cryptographic Failures, and Logging/Monitoring deficiencies.

Think of your Node.js app like a house.

Production security checklists include dependency scanning (npm audit, Snyk), valid HTTPS configuration, and regular security header audits.

Plain-English First

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.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

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.

threat-model.jsJAVASCRIPT
1
2
3
4
5
6
7
// Not runnable, but a mental model
const owaspCategories = {
  brokenAccessControl: 'Rate limiting, CORS, CSP',
  injection: 'Input validation, parameterized queries',
  // ...
};
console.log('Map middleware to OWASP categories');
Output
Map middleware to OWASP categories
Try it live
🔥OWASP Top 10 is not optional
Even if you use Helmet and rate limiting, you must understand the underlying threats. Otherwise, you'll miss edge cases like mass assignment or SSRF.
📊 Production Insight
In production, we once saw a team that had Helmet but no rate limiting. A single endpoint was hammered with 10k requests/sec, bypassing all other controls. Rate limiting would have stopped it.
🎯 Key Takeaway
Map every security middleware to an OWASP category to ensure coverage.
nodejs-security-helmet-rate-limit THECODEFORGE.IO Node.js Security Layer Stack Layered defense architecture for a Node.js application Network Edge Rate Limiter | CORS Middleware HTTP Headers Helmet (CSP, HSTS, X-Frame-Opt Application Logic Input Validation | Sanitization | Secure Sessions Data Access Parameterized Queries | ORM Security Dependency Management npm audit | Dependency Updates Monitoring & Logging Error Handling | Audit Logs THECODEFORGE.IO
thecodeforge.io
Nodejs Security Helmet Rate Limit

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.

helmet-setup.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const express = require('express');
const helmet = require('helmet');

const app = express();

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "https://cdn.example.com"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:"],
    },
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true,
  },
}));

app.get('/', (req, res) => {
  res.send('Hello secure world!');
});

app.listen(3000);
Output
Server listening on port 3000 with security headers set.
Try it live
⚠ CSP can break your app
If you set CSP too strict, inline scripts or eval() will be blocked. Test thoroughly in staging before deploying to production.
📊 Production Insight
We once saw a production outage because Helmet's default CSP blocked inline scripts from a third-party analytics tool. Always whitelist external domains explicitly.
🎯 Key Takeaway
Helmet sets essential security headers, but CSP must be tailored to your app's resources.

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.

rate-limit.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const redisClient = require('./redis-client'); // assume configured

const globalLimiter = rateLimit({
  store: new RedisStore({
    sendCommand: (...args) => redisClient.sendCommand(args),
  }),
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Too many requests, please try again later.' },
});

const loginLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 5, // limit each IP to 5 login requests per windowMs
  message: { error: 'Too many login attempts, please try again after a minute.' },
});

app.use(globalLimiter);
app.use('/login', loginLimiter);
Output
Rate limiting applied globally and on /login endpoint.
Try it live
💡Use Redis for production rate limiting
In-memory rate limiting resets on server restart. Redis ensures persistence and consistency across multiple instances.
📊 Production Insight
A client once had a server restart during a brute-force attack. The in-memory rate limit reset, allowing thousands of login attempts in seconds. Redis would have prevented this.
🎯 Key Takeaway
Rate limiting is critical for login endpoints; use Redis-backed stores for production.
nodejs-security-helmet-rate-limit THECODEFORGE.IO Node.js Security Architecture Layers Layered defense for a Node.js application Network Layer Rate Limiter | CORS Policy | HTTPS/TLS HTTP Layer Helmet Headers | Content Security Policy | HSTS Application Layer Input Validation | Session Management | Authentication Data Layer Parameterized Queries | Encryption | Audit Logging Dependency Layer npm Audit | Package Updates | Snyk Scanner THECODEFORGE.IO
thecodeforge.io
Nodejs Security Helmet Rate Limit

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.

cors-setup.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const cors = require('cors');

const allowedOrigins = ['https://myapp.com', 'https://admin.myapp.com'];

const corsOptions = {
  origin: function (origin, callback) {
    // allow requests with no origin (like mobile apps or curl)
    if (!origin) return callback(null, true);
    if (allowedOrigins.indexOf(origin) !== -1) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,
};

app.use(cors(corsOptions));
Output
CORS configured to allow only specific origins.
Try it live
⚠ Never use wildcard CORS with credentials
Setting Access-Control-Allow-Origin: * with credentials: true is invalid and will be rejected by browsers.
📊 Production Insight
A startup used origin: '*' for their API. An attacker hosted a malicious site that made authenticated requests from users' browsers, stealing data. Always whitelist.
🎯 Key Takeaway
Restrict CORS to specific origins and never use wildcards with credentials.

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.

input-validation.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const { body, validationResult } = require('express-validator');

app.post('/user',
  body('email').isEmail().normalizeEmail(),
  body('name').trim().isLength({ min: 1, max: 100 }),
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    // safe to use req.body
    res.send('User created');
  }
);
Output
POST /user with invalid email returns 400 with error details.
Try it live
🔥Sanitize, don't just validate
Validation rejects bad input, but sanitization cleans it. For example, strip HTML tags from user names before storing.
📊 Production Insight
We saw a MongoDB injection that used $gt to bypass authentication. Input validation that rejected $ in username fields would have stopped it.
🎯 Key Takeaway
Always validate and sanitize input; use parameterized queries to prevent injection.

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.

session-setup.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const redisClient = require('./redis-client');

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: true, // only send over HTTPS
    httpOnly: true, // not accessible via JavaScript
    sameSite: 'strict',
    maxAge: 24 * 60 * 60 * 1000, // 24 hours
  },
}));
Output
Session configured with secure, HTTP-only cookies and Redis store.
Try it live
💡Use environment variables for secrets
Never hardcode session secrets or JWT secrets. Use environment variables and rotate them periodically.
📊 Production Insight
A production app stored JWT in localStorage. An XSS vulnerability allowed attackers to steal tokens. Switching to HTTP-only cookies prevented this.
🎯 Key Takeaway
Use secure, HTTP-only cookies with Redis-backed sessions and bcrypt for passwords.

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.

error-handler.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const winston = require('winston');

const logger = winston.createLogger({
  level: 'error',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log' }),
  ],
});

app.use((err, req, res, next) => {
  logger.error({
    message: err.message,
    stack: err.stack,
    requestId: req.id,
  });
  res.status(err.status || 500).json({
    error: 'Internal server error',
  });
});

process.on('unhandledRejection', (reason) => {
  logger.error({ message: 'Unhandled Rejection', reason });
  process.exit(1); // or gracefully shutdown
});
Output
Error logged to file, client receives generic message.
Try it live
⚠ Don't expose stack traces in production
Stack traces reveal file paths and code structure. Always log them server-side and return a generic error to the client.
📊 Production Insight
A production app leaked database table names in error messages. An attacker used that info to craft SQL injection payloads. Always sanitize error output.
🎯 Key Takeaway
Centralize error handling, log full errors, and return generic messages to clients.

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.

audit.shBASH
1
2
3
npm audit --audit-level=high
# or
npx snyk test
Output
Found 0 vulnerabilities (or list of vulnerabilities with severity)
🔥Automate dependency audits
Run npm audit in CI and fail the build if high-severity vulnerabilities are found. Use Dependabot for automatic PRs.
📊 Production Insight
The event-stream incident (malicious package) affected many apps. If they had automated auditing, they would have detected the suspicious behavior earlier.
🎯 Key Takeaway
Regularly audit and update dependencies; automate vulnerability scanning in CI.

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.

custom-headers.jsJAVASCRIPT
1
2
3
4
5
6
7
app.use((req, res, next) => {
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
  res.setHeader('Permissions-Policy', 'geolocation=(), microphone=()');
  next();
});
Output
Custom security headers set on every response.
Try it live
💡Test your headers with securityheaders.com
This free tool grades your site's security headers and gives recommendations. Aim for an A+ rating.
📊 Production Insight
A client's site had an A+ rating on securityheaders.com, but they forgot to set HSTS preload. Users on HTTP were vulnerable to downgrade attacks until they added preload.
🎯 Key Takeaway
Understand each security header and configure them beyond Helmet defaults.

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.

security-checklist.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const checklist = [
  'Helmet with custom CSP',
  'Rate limiting with Redis',
  'CORS whitelist',
  'Input validation',
  'Secure sessions',
  'Error handling',
  'Dependency audit',
  'Security headers',
  'HTTPS enforced',
  'Account lockout',
  'Environment secrets',
  'Monitoring',
];
console.log('Production security checklist:', checklist);
Output
Production security checklist: [ ... ]
Try it live
🔥Automate the checklist
Use tools like OWASP ZAP or Burp Suite to scan your app automatically. Integrate security tests into CI.
📊 Production Insight
We use a security checklist in our CI pipeline. It catches misconfigurations before they reach production, like missing HSTS or open CORS.
🎯 Key Takeaway
Security is a continuous process; use a checklist and automate scanning.

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.

monitoring.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const prometheus = require('prom-client');

const httpRequestDuration = new prometheus.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status'],
  buckets: [0.1, 0.5, 1, 2, 5],
});

app.use((req, res, next) => {
  const end = httpRequestDuration.startTimer();
  res.on('finish', () => {
    end({ method: req.method, route: req.route?.path || 'unknown', status: res.statusCode });
  });
  next();
});
Output
Prometheus metrics exposed at /metrics.
Try it live
⚠ Don't ignore alerts
Alert fatigue is real. Set meaningful thresholds and have an on-call rotation. Every alert should have a runbook.
📊 Production Insight
We once detected a DDoS attack because our rate limit alerts fired. We quickly scaled up and blocked the offending IPs, minimizing downtime.
🎯 Key Takeaway
Monitor your app for anomalies and have an incident response plan.

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.

mindset.txtTEXT
1
2
Security is a mindset, not a checklist.
Stay updated, stay vigilant.
🔥Keep learning
The threat landscape evolves. Subscribe to security newsletters, follow OWASP, and review your security posture regularly.
📊 Production Insight
The most secure teams we've seen treat security as a continuous improvement process, with regular audits and a culture of security awareness.
🎯 Key Takeaway
Security is an ongoing commitment, not a one-time setup.

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.

ownershipCheck.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const ownershipCheck = (model, paramName = 'id') => {
  return async (req, res, next) => {
    try {
      const resource = await model.findByPk(req.params[paramName]);
      if (!resource) return res.status(404).json({ error: 'Not found' });
      if (resource.userId !== req.user.id) {
        return res.status(403).json({ error: 'Forbidden' });
      }
      req.resource = resource;
      next();
    } catch (err) {
      next(err);
    }
  };
};

// Usage in route:
router.get('/orders/:id', authenticate, ownershipCheck(Order, 'id'), (req, res) => {
  res.json(req.resource);
});
Output
Returns 403 if order.userId !== req.user.id
Try it live
⚠ Don't Forget Nested Resources
BOLA also applies to nested resources. For example, DELETE /users/:userId/orders/:orderId must verify that the order belongs to the user AND that the authenticated user matches :userId.
📊 Production Insight
In microservices, BOLA can cross service boundaries. Use a centralized authorization service or propagate user context via JWT claims.
🎯 Key Takeaway
Every endpoint that accesses a resource by ID must verify ownership. Use reusable middleware to enforce this consistently.

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.

hppSetup.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const express = require('express');
const hpp = require('hpp');

const app = express();

// Apply HPP protection globally
app.use(hpp({
  whitelist: ['tags', 'categories'] // allow duplicate arrays
}));

// Example vulnerable endpoint without hpp
app.get('/api/users', (req, res) => {
  // Without hpp, ?role=user&role=admin could set role to ['user','admin']
  const role = req.query.role;
  res.json({ role });
});
Output
With hpp, ?role=user&role=admin results in role = 'admin' (last value) or 400 if not whitelisted.
Try it live
💡Combine with Input Validation
HPP is a defense-in-depth measure. Always validate and sanitize parameters regardless of HPP protection.
📊 Production Insight
HPP can also affect POST bodies if your parser merges duplicates. Use strict parsing (e.g., express.json({ strict: true })) and consider rejecting duplicate keys.
🎯 Key Takeaway
Use hpp middleware to prevent HTTP Parameter Pollution attacks. Whitelist parameters that are intentionally arrays.

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.

rateLimiters.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const rateLimit = require('express-rate-limit');

// Global limiter: 100 req/min
const globalLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 100,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Too many requests, please try again later.' }
});

// Login limiter: 5 attempts per 15 minutes
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  skipSuccessfulRequests: true, // only count failures
  message: { error: 'Too many login attempts, please try again after 15 minutes.' }
});

app.use('/api/', globalLimiter);
app.use('/api/auth/login', loginLimiter);
Output
Global: 100 req/min. Login: 5 req/15min.
Try it live
🔥Why 5/15min?
Based on OWASP ASVS and common brute-force tool speeds. Adjust based on your user base and risk tolerance.
📊 Production Insight
Use Redis as a store for rate limit counters to ensure consistency across multiple server instances. Consider per-user rate limiting for authenticated endpoints.
🎯 Key Takeaway
Apply strict rate limits (e.g., 5/15min) on authentication endpoints. Use separate limiters for different sensitivity levels.

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.

jwtRefreshFlow.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const jwt = require('jsonwebtoken');
const crypto = require('crypto');

// Generate tokens
function generateTokens(userId) {
  const accessToken = jwt.sign({ userId }, process.env.ACCESS_SECRET, { expiresIn: '15m' });
  const refreshToken = crypto.randomBytes(40).toString('hex');
  // Store hashed refresh token in DB
  storeRefreshToken(userId, crypto.createHash('sha256').update(refreshToken).digest('hex'));
  return { accessToken, refreshToken };
}

// Refresh endpoint
app.post('/api/auth/refresh', async (req, res) => {
  const { refreshToken } = req.cookies;
  if (!refreshToken) return res.sendStatus(401);
  const hashed = crypto.createHash('sha256').update(refreshToken).digest('hex');
  const stored = await findRefreshToken(hashed);
  if (!stored) return res.sendStatus(403);
  // Rotate refresh token
  const tokens = generateTokens(stored.userId);
  res.cookie('refreshToken', tokens.refreshToken, { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000 });
  res.json({ accessToken: tokens.accessToken });
});
Output
Access token expires in 15 min. Refresh token in HttpOnly cookie, rotated on each use.
Try it live
⚠ Refresh Token Rotation
Always issue a new refresh token on each refresh and invalidate the old one. This limits the damage if a refresh token is stolen.
📊 Production Insight
Consider refresh token expiration based on user activity. Inactive sessions can expire sooner. Use a deny list for immediate revocation of compromised tokens.
🎯 Key Takeaway
Use short-lived access tokens (15 min) with refresh tokens stored in HttpOnly cookies. Rotate refresh tokens and store them server-side for revocation.
Helmet vs Manual Header Security Comparing automated header hardening with manual configuration Helmet Middleware Manual Headers Ease of Setup Single npm install and use() call Requires custom header logic per route Coverage 15+ security headers automatically Must manually add each header Updates Regular updates for new threats No automatic updates; manual maintenance Customization Flexible options for each header Full control but error-prone Best Practice Aligns with OWASP recommendations Risk of missing critical headers THECODEFORGE.IO
thecodeforge.io
Nodejs Security Helmet Rate Limit

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.

ci-audit.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash
# Run in CI
npm audit --audit-level=high
if [ $? -ne 0 ]; then
  echo "High severity vulnerabilities found. Failing build."
  exit 1
fi

# Snyk (requires SNYK_TOKEN)
npx snyk test --severity-threshold=high
if [ $? -ne 0 ]; then
  echo "Snyk found vulnerabilities. Failing build."
  exit 1
fi
Output
Exits with code 1 if high-severity vulnerabilities found.
💡Automate Updates
Use Dependabot or Renovate to create PRs for dependency updates automatically. Review and merge promptly.
📊 Production Insight
For critical applications, consider a software composition analysis (SCA) tool like Snyk or WhiteSource that provides license compliance and vulnerability prioritization.
🎯 Key Takeaway
Run npm audit and Snyk in CI with high-severity thresholds. Automate dependency updates with Dependabot or Renovate.
● Production incidentPOST-MORTEMseverity: high

Rate Limiter Misconfiguration Causes Global API Outage

Symptom
All API requests returned HTTP 429 Too Many Requests after the first successful call. The error rate spiked to 100% within seconds of deployment.
Assumption
The rate limiter was keyed on client IP, and the load balancer was configured to forward the original IP via X-Forwarded-For. We assumed the middleware would correctly parse that header.
Root cause
The express-rate-limit middleware was not configured with trustProxy: true, so it used the load balancer's internal IP for all requests. Since all requests appeared to come from the same IP, the first request exhausted the limit, blocking all subsequent requests.
Fix
Set trustProxy: true in the rate limiter configuration and verified that the X-Forwarded-For header was correctly set by the load balancer. Also added a health check endpoint exempt from rate limiting.
Key lesson
  • 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.
⚙ Quick Reference
17 commands from this guide
FileCommand / CodePurpose
threat-model.jsconst owaspCategories = {The OWASP Top 10
helmet-setup.jsconst express = require('express');Helmet
rate-limit.jsconst rateLimit = require('express-rate-limit');Rate Limiting
cors-setup.jsconst cors = require('cors');CORS
input-validation.jsconst { body, validationResult } = require('express-validator');Input Validation and Sanitization
session-setup.jsconst session = require('express-session');Secure Session Management and Authentication
error-handler.jsconst winston = require('winston');Error Handling and Logging
audit.shnpm audit --audit-level=highDependency Security
custom-headers.jsapp.use((req, res, next) => {Security Headers Beyond Helmet
security-checklist.jsconst checklist = [Putting It All Together
monitoring.jsconst prometheus = require('prom-client');Monitoring and Incident Response
mindset.txtSecurity is a mindset, not a checklist.Conclusion
ownershipCheck.jsconst ownershipCheck = (model, paramName = 'id') => {BOLA (Broken Object Level Authorization)
hppSetup.jsconst express = require('express');HPP (HTTP Parameter Pollution) Protection with hpp
rateLimiters.jsconst rateLimit = require('express-rate-limit');Rate Limiting
jwtRefreshFlow.jsconst jwt = require('jsonwebtoken');JWT + Refresh Token Flow
ci-audit.shnpm audit --audit-level=highDependency Auditing

Key takeaways

1
Map middleware to OWASP categories
Every security middleware should address a specific OWASP risk. This ensures you're not just checking boxes but building a coherent defense.
2
Customize Helmet's CSP for production
Default CSP is too restrictive. Tailor it to your app's resources, but test thoroughly to avoid breaking functionality.
3
Use Redis-backed rate limiting
In-memory rate limiting resets on restart, leaving you vulnerable. Redis ensures persistence and consistency across instances.
4
Security is a continuous process
Regular audits, monitoring, and incident response are as important as initial configuration. Automate what you can and stay informed.
5
BOLA (Broken Object Level Authorization)
Implement ownership-check middleware for every resource endpoint. Never trust client-supplied IDs without verifying the authenticated user's ownership.
6
HPP Protection
Use the hpp middleware to prevent HTTP Parameter Pollution. Whitelist parameters that are intentionally arrays.
7
Rate Limiting Specifics
Apply 5 requests per 15 minutes for login endpoints and 100 requests per minute for global endpoints. Use separate limiters and a distributed store like Redis.
8
BOLA (Broken Object Level Authorization)
Always verify that the authenticated user owns the requested resource. Use a reusable ownership-check middleware to avoid missing this critical check on any endpoint.
9
Rate limiting by endpoint
Apply strict limits (e.g., 5 requests per 15 minutes) on login endpoints and generous limits (e.g., 100 per minute) on general API. Never use the same limit for both.
10
JWT + refresh token flow
Use short-lived access tokens (15 min) and rotate refresh tokens stored in HTTP-only cookies. This limits the impact of token theft and allows revocation.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What does the Helmet middleware do in an Express app?
Q02SENIOR
How would you implement rate limiting in a Node.js API to prevent brute-...
Q03SENIOR
Explain how you would protect against SQL injection in a Node.js applica...
Q04SENIOR
Describe a scenario where Helmet's default settings might break a legiti...
Q05SENIOR
How would you handle rate limiting in a microservices architecture where...
Q06SENIOR
What is the most critical OWASP Top 10 vulnerability for a Node.js REST ...
Q01 of 06JUNIOR

What does the Helmet middleware do in an Express app?

ANSWER
Helmet sets various HTTP security headers to protect against common web vulnerabilities. For example, it sets X-Content-Type-Options to prevent MIME sniffing, X-Frame-Options to prevent clickjacking, and Strict-Transport-Security to enforce HTTPS.
FAQ · 12 QUESTIONS

Frequently Asked Questions

01
What is the OWASP Top 10 and why should I care?
02
How do I choose between Helmet's default CSP and a custom one?
03
Why should I use Redis for rate limiting instead of in-memory storage?
04
Can I use Helmet and CORS together?
05
What is the best way to store JWT tokens?
06
How often should I run dependency audits?
07
What is the difference between BOLA and IDOR?
08
Should I use npm audit or Snyk?
09
How do I revoke a JWT before it expires?
10
How do I implement ownership checks without repeating code in every route?
11
What's the difference between npm audit and Snyk? Should I use both?
12
How do I handle rate limiting for login when attackers rotate IPs?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Written from production experience, not tutorials.

Follow
Verified
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
🔥

That's Node.js. Mark it forged?

6 min read · try the examples if you haven't

Previous
File Upload in Node.js with Multer
30 / 47 · Node.js
Next
Logging in Node.js with Winston and Pino