Skip to main content
interlace
Plugin: express-securityRules

require-helmet

This rule detects Express.js applications that are missing the helmet middleware

Require helmet middleware for security headers in Express.js applications

Severity: πŸ”΄ High
CWE: CWE-693

Value & investment case

Why this rule pays for itself. Framework: cicd-impact/philosophy.md.

DimensionValue
CWECWE-693 β€” Protection Mechanism Failure (missing security headers)
Feedback-loop tierEditor / pre-commit (sub-second) β€” cheapest layer per the feedback-loop hierarchy
Defensive-layer leverage~10Γ— cheaper than unit-test Β· ~1,000Γ— cheaper than production rollback Β· 10,000+Γ— cheaper than customer disclosure (cost-ratio anchors)
Niche relevanceCritical: B2B SaaS, B2C (any browser-facing surface) Β· High: fintech, healthtech, marketplaces Β· Medium: infra/devtools
Investor-frame impactMissing Helmet β†’ no CSP, X-Frame-Options, HSTS, etc. Defense-in-depth gap that SOC2 Common Criteria CC6.6 explicitly addresses. Lint-time enforcement = audit-grade evidence.

Read also: philosophy.md Β§investor-frame Β· niche-presets.json Β· analyzer-evaluation-framework.md

Rule Details

This rule detects an Express application that renders documents to a browser without mounting helmet.

Missing security headers can expose a rendered page to:

  • Clickjacking attacks (X-Frame-Options)
  • XSS attacks (X-XSS-Protection, Content-Security-Policy)

The document precondition

Helmet's distinctive headers are instructions to a renderer β€” they are inert on a response nothing renders. The rule therefore requires evidence that this app returns documents: res.render(…), res.sendFile(…), or a configured view engine (app.set('view engine', …) / app.engine(…)).

res.send(…) is deliberately not evidence: it is the generic responder for JSON, text and buffers, and a linter cannot tell a document from a payload without reading it.

The two headers that do apply to a machine client are owned elsewhere in this plugin β€” require-strict-transport-security (HSTS) and no-disabled-helmet-protections (X-Content-Type-Options) β€” so the precondition removes a duplicate finding rather than a unique one.

The escape hatch

If the app binding leaves the file β€” module.exports = app, export default app, return app, or setAppConfigurations(app) β€” the middleware stack is assembled somewhere this rule cannot see, and it abstains. Absence of evidence is not evidence of absence.

Partition with require-rate-limiting

Both rules used to report the identical express() node. require-rate-limiting now reports the specific unthrottled credential endpoint instead, so the two are disjoint by construction.

Examples

❌ Incorrect

import express from 'express';
const app = express();

// Renders documents, no helmet - VULNERABLE
app.set('view engine', 'pug');
app.get('/', (req, res) => res.render('home'));

app.listen(3000);

βœ… Correct

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

const app = express();

// Helmet adds security headers
app.use(helmet());

app.set('view engine', 'pug');
app.get('/', (req, res) => res.render('home'));

app.listen(3000);
// A static asset server renders nothing itself. Not reported.
const app = express();
app.use(express.static('./public'));
app.listen(8080);

// The app is configured by its importer. Not reported.
const app2 = express();
module.exports = app2;

Options

OptionTypeDefaultDescription
allowInTestsbooleanfalseAllow missing helmet in test files
alternativeMiddlewarestring[][]Alternative security headers middleware names to accept
assumeHelmetMiddlewarebooleanfalseSkip rule if security headers are provided elsewhere (e.g., reverse proxy)
{
  "rules": {
    "express-security/require-helmet": [
      "error",
      {
        "allowInTests": true,
        "alternativeMiddleware": ["securityHeaders"]
      }
    ]
  }
}

When Not To Use It

Never disable this rule in production. Security headers are a fundamental protection layer.

Known False Negatives

The following patterns are not detected due to static analysis limitations:

App Instance from Variable

Why: Express app stored in variable may not be recognized.

// ❌ NOT DETECTED - App from factory function
const app = createExpressApp();
// Helmet might be applied in createExpressApp, or not

Mitigation: Apply rule to factory modules. Document helmet usage centrally.

Conditional Middleware

Why: Middleware applied inside conditions is not tracked.

// ❌ NOT DETECTED - Conditional helmet
if (process.env.NODE_ENV === 'production') {
  app.use(helmet());
}
// Development may run without headers

Mitigation: Always apply helmet unconditionally. Use environment-specific configuration inside helmet options.

Framework Wrappers

Why: Higher-level frameworks may include helmet internally.

// ❌ FALSE POSITIVE RISK - Framework includes helmet
import { createServer } from '@my-company/express-framework';
const app = createServer(); // May include helmet

Mitigation: Configure alternativeMiddleware option. Add framework-specific patterns.

Late Middleware Application

Why: Helmet applied after route definitions is less effective.

// ❌ NOT DETECTED - Helmet AFTER routes
app.get('/api', handler);
app.use(helmet()); // Security headers won't apply to /api

Mitigation: Ensure helmet is among the first middleware. Review middleware order in code review.

Custom Security Headers

Why: Manual header setting without helmet is not recognized.

// ❌ NOT DETECTED - Manual headers instead of helmet
app.use((req, res, next) => {
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('X-Content-Type-Options', 'nosniff');
  next();
});

Mitigation: Use helmet for comprehensive coverage. Configure alternativeMiddleware for known patterns.

Further Reading

Did this rule catch something? Star the repo to get new CWE coverage as we ship it β€” or follow the AI-code-security benchmarks behind these rules.