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¶
- sec_04_auth.md - authentication basics before headers
next -> sec_06_rate_limiting.md