Skip to content

Helmet.js and Security Headers

Your app is leaking information through HTTP headers Every response Express sends by default tells attackers what framework you're running , what version , and that you accept sneaky content types Helmet.js plugs 14+ security headers into your Express app with zero config

Basic Setup

const express = require('express')
const helmet = require('helmet')
const app = express()

// Apply all default protections
app.use(helmet())

// Check what headers are now set
app.get('/headers', (req, res) => {
  res.json({
    headers: res.getHeaders()
  })
})
// -> x-frame-options: DENY
// -> x-content-type-options: nosniff
// -> strict-transport-security: max-age=15552000; includeSubDomains
// -> x-dns-prefetch-control: off
// -> x-download-options: noopen
// -> x-permitted-cross-domain-policies: none

Helmet is not optional : it's 15 lines of configuration that blocks entire classes of attacks If you're not using it , you're serving XSS vulnerabilities with every response

Content Security Policy (CSP)

CSP prevents XSS by telling the browser what content sources are allowed

const helmet = require('helmet')

app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "'strict-dynamic'"],
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", 'data:', 'https://trusted-cdn.com'],
    connectSrc: ["'self'", 'https://api.example.com'],
    fontSrc: ["'self'", 'https://fonts.googleapis.com'],
    objectSrc: ["'none'"],
    baseUri: ["'self'"],
    formAction: ["'self'"],
    frameAncestors: ["'none'"],
    upgradeInsecureRequests: []
  },
  reportOnly: false  // set true to test before enforcing
}))

// With reporting endpoint
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'"],
    reportUri: '/csp-violation'
  },
  reportOnly: true  // log violations , don't block
}))

app.post('/csp-violation', express.json(), (req, res) => {
  console.error('CSP Violation:', req.body)
  // log to your security monitoring
  res.status(204).end()
})

Start with reportOnly: true : CSP will break things if you're too aggressive Check violations , tune directives , then enforce

Strict-Transport-Security (HSTS)

Tells browsers to always use HTTPS for your domain

const helmet = require('helmet')

// Standard HSTS - enforce HTTPS for 1 year
app.use(helmet.hsts({
  maxAge: 31536000,        // 1 year in seconds
  includeSubDomains: true, // apply to all subdomains
  preload: true            // submit to browser preload lists
}))

// Development - zero maxAge to disable locally
if (process.env.NODE_ENV === 'development') {
  app.use(helmet.hsts({ maxAge: 0 }))
}

HSTS preload is permanent : once your domain is in the Chrome/Chromium preload list , it's almost impossible to remove Test before enabling preload

Frame Options and Content Type

const helmet = require('helmet')

// Prevent clickjacking - deny all framing
app.use(helmet.frameguard({ action: 'deny' }))

// Allow same-origin framing only
app.use(helmet.frameguard({ action: 'sameorigin' }))

// Prevent MIME type sniffing
app.use(helmet.noSniff())
// X-Content-Type-Options: nosniff
// Browser won't try to guess content type - must match declared type

// Prevent IE from executing downloads in site context
app.use(helmet.ieNoOpen())
// X-Download-Options: noopen

frameguard({ action: 'deny' }) for most apps : only allow framing if you're building a widget or embeddable component

Referrer Policy

Controls what info is sent in the Referer header when users click links

const helmet = require('helmet')

// Most secure - send referrer for same-origin only
app.use(helmet.referrerPolicy({
  policy: 'strict-origin-when-cross-origin'
}))

// Other options from most to least secure:
// - no-referrer                   - never send
// - same-origin                   - only same origin
// - strict-origin-when-cross-origin - secure only , full URL same-origin
// - origin                        - always send origin only
// - unsafe-url                    - full URL always (DON'T USE)

strict-origin-when-cross-origin is the sweet spot : sends full URL on same-origin requests , origin-only on cross-origin , nothing on downgrade (HTTPS -> HTTP)

Permissions Policy

Controls what browser APIs your app can access

const helmet = require('helmet')

app.use(helmet.permissionsPolicy({
  policy: {
    microphone: [],
    camera: [],
    geolocation: [],
    payment: [],
    usb: [],
    accelerometer: [],
    autoplay: ["'self'"],
    encryptedMedia: ["'self'"]
  }
}))

// More concise with featuresPolicy
app.use(helmet({
  crossOriginEmbedderPolicy: true,
  crossOriginOpenerPolicy: { policy: 'same-origin' },
  crossOriginResourcePolicy: { policy: 'same-origin' }
}))

Block everything by default : only enable APIs your app actually needs Users don't trust random sites accessing their microphone

Custom Helmet Configuration

const helmet = require('helmet')

app.use(helmet({
  // Enable/disable specific protections
  contentSecurityPolicy: false,  // you'll set this manually
  crossOriginEmbedderPolicy: { policy: 'require-corp' },
  dnsPrefetchControl: { allow: false },
  frameguard: { action: 'deny' },
  hidePoweredBy: true,                 // removes X-Powered-By
  hsts: { maxAge: 31536000 },
  ieNoOpen: true,
  noSniff: true,
  originAgentCluster: true,
  permittedCrossDomainPolicies: { permittedPolicies: 'none' },
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
  xssFilter: true                       // legacy XSS filter
}))

Disable individual protections if they conflict with your app : CSP often needs custom configuration for third-party scripts and CDNs

Testing Security Headers

// Quick curl test
curl -I https://yoursite.com | grep -i '^x-\|^strict-\|^content-security\|^referrer'

// Node script to check headers
const https = require('https')
function checkHeaders(url) {
  https.get(url, (res) => {
    const essential = [
      'strict-transport-security',
      'x-content-type-options',
      'x-frame-options',
      'content-security-policy',
      'referrer-policy',
      'permissions-policy'
    ]
    for (const header of essential) {
      const present = res.headers[header] ? 'PASS' : 'FAIL'
      console.log(`${present}: ${header}`)
    }
  })
}
checkHeaders('https://yoursite.com')

Run your app through securityheaders.com : it grades A+ through F based on header coverage Don't ship until you get at least A

Prerequisites

next -> sec_06_rate_limiting.md