Vulnerability
Express

Missing Security Headers in Express.js

Express.js sends no security headers by default. Without the Helmet middleware or manual header configuration, responses lack Content-Security-Policy, Strict-Transport-Security, and X-Content-Type-Options — leaving the application exposed to XSS, clickjacking, MIME sniffing, and protocol downgrade attacks.

Scan Your Express App

How Missing Security Headers Manifests in Express

A freshly created Express application sends only basic headers like Content-Type and Content-Length. Every security-relevant header must be added explicitly. Developers often skip this step during initial development and forget to add it before production deployment. Even when Helmet is installed, developers frequently use helmet() with default settings without understanding what each header does. The default Helmet configuration is a good baseline but may not include a Content-Security-Policy or may set it too permissively for your application. Express apps that serve both an API and static files may apply Helmet only to API routes, leaving static file responses without security headers. Similarly, error responses from custom error handlers often bypass middleware, sending responses without security headers. CORS misconfiguration is adjacent to this issue: Express apps using cors() with { origin: '*', credentials: true } effectively disable same-origin protections.

Real-World Impact

An Express API serving a React frontend was deployed without Helmet. The app had a minor XSS vulnerability in a search feature. Without CSP headers, the attacker's injected script ran unrestricted, exfiltrating session tokens. With even a basic CSP of script-src 'self', the injected inline script would have been blocked. Another Express application served user-uploaded files as attachments. Without X-Content-Type-Options: nosniff, browsers reinterpreted a malicious .txt file as HTML, executing JavaScript embedded in the file when opened in the browser.

Step-by-Step Fix

1

Install and configure Helmet

Add Helmet as the first middleware to set all baseline security headers.

import express from 'express';
import helmet from 'helmet';

const app = express();

// Apply Helmet first, before any other middleware
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", 'data:', 'https:'],
      connectSrc: ["'self'"],
      fontSrc: ["'self'"],
      frameSrc: ["'none'"],
      frameAncestors: ["'none'"],
    },
  },
  strictTransportSecurity: {
    maxAge: 63072000,
    includeSubDomains: true,
    preload: true,
  },
}));
2

Ensure error responses include headers

Custom error handlers in Express may bypass middleware. Verify security headers are on error responses too.

// Error handler that preserves security headers
// (Helmet already set headers on the response object)
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
  console.error(err.stack);
  // Headers from Helmet are already set because
  // Helmet runs before this handler.
  // Do NOT create a new response object.
  res.status(500).json({ error: 'Internal server error' });
});

// WRONG - this creates a response without going through Helmet:
// app.use((err, req, res, next) => {
//   const newRes = new http.ServerResponse(req);
//   newRes.end('Error');
// });
3

Configure Permissions-Policy

Helmet does not set Permissions-Policy by default. Add it manually to restrict browser features.

// Helmet sets most headers, but add Permissions-Policy separately
app.use(helmet());
app.use((req, res, next) => {
  res.setHeader(
    'Permissions-Policy',
    'camera=(), microphone=(), geolocation=(), payment=()'
  );
  next();
});

// Or combine into a single middleware
app.use((req, res, next) => {
  // Check if your app uses geolocation before blocking it
  res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
  next();
});
4

Audit all response paths for header coverage

Verify that security headers appear on every type of response: API, static files, redirects, and errors.

// Quick test script
import fetch from 'node-fetch';

const urls = [
  'http://localhost:3000/',          // Home page
  'http://localhost:3000/api/health', // API route
  'http://localhost:3000/static/logo.png', // Static file
  'http://localhost:3000/nonexistent', // 404 error
];

const EXPECTED = [
  'content-security-policy',
  'strict-transport-security',
  'x-content-type-options',
  'x-frame-options',
];

for (const url of urls) {
  const res = await fetch(url);
  const missing = EXPECTED.filter(h => !res.headers.get(h));
  console.log(`${url}: ${missing.length === 0 ? 'OK' : 'MISSING: ' + missing.join(', ')}`);
}

Prevention Best Practices

1. Install and configure Helmet as the first middleware in your Express app. 2. Customize Helmet's CSP directives for your specific application needs. 3. Ensure security headers are set on ALL responses including error pages and static files. 4. Set HSTS with a long max-age once HTTPS is properly configured. 5. Review CORS configuration to ensure it is not overly permissive. 6. Test headers on every response type your server produces.

How to Test

1. Run curl -I http://localhost:3000/ and check which security headers are present. 2. Verify that API routes, static files, 404 pages, and error pages all include security headers. 3. Check for X-Powered-By: Express (Helmet removes this by default, but verify). 4. Test that CSP blocks inline scripts by injecting <script>alert(1)</script> into a reflected parameter. 5. Use Vibe App Scanner to automatically detect missing security headers across all your Express endpoints.

Frequently Asked Questions

Does Helmet set all the security headers I need?

Helmet sets most essential headers by default: X-Content-Type-Options, X-Frame-Options, X-DNS-Prefetch-Control, X-Download-Options, X-Permitted-Cross-Domain-Policies, Referrer-Policy, and Strict-Transport-Security. However, Content-Security-Policy requires customization for your app, and Permissions-Policy is not included by default.

Will adding security headers break my Express API?

For pure JSON APIs, most headers have no effect on clients. CSP only affects browser rendering, so API consumers ignore it. However, if your Express app serves HTML pages, a strict CSP can break inline scripts and third-party resources. Start with report-only mode to identify issues.

Should I use Helmet or set headers manually?

Use Helmet. It maintains secure defaults that are updated as best practices evolve. Manually setting headers requires tracking each header's correct syntax and recommended values. Helmet also handles edge cases like removing the X-Powered-By header that reveals your tech stack.

Is Your Express App Vulnerable to Missing Security Headers?

VAS automatically scans for missing security headers vulnerabilities in Express applications and provides step-by-step remediation guidance with code examples.

Scans from $5, results in minutes. Get actionable fixes tailored to your Express stack.