How to use Helmet in Express.js
Written by Ulises Gascón
Apr 08, 2020 — 6 min readUnderstanding Helmet
If you include Express on your stack, I highly recommend to extend the HTTP Headers definitions using the middleware Helmet
I just adapted and extended the Official documentation from Helmet in order to adapt the content to our needs as follow:
- Content Security Policy
- Cross Domain Policies
- DNS Prefetch Control
- Don't Sniff Mimetype
- Expect Certificate Transparency
- Feature Policy
- Frameguard
- Strict Transport Security
- IE No Open
- Powered by
- Referrer Policy
- XSS Filter
Using Helmet
Helmet is a very simple and customizable. We will split the setup in three steps (Basic, extended and evolutionary)
Basic Setup
By default Helmet will handle several headers just by adding the middleware to the project
Headers in scope
- ✅ DNS Prefetch Control
- ✅ Frameguard
- ✅ Hide powered by
- ✅ Strict Transport Security
- ✅ IE No Open
- ✅ Don't Sniff Mimetype
- ✅ XSS Filter
Code
const helmet = require('helmet')
app.use(helmet())
Response headers
Express by default without helmet:
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 12
ETag: W/"c-Lve95gjOVATpfV8EL5X4nxwjKHE"
Date: Wed, 08 Apr 2020 07:00:48 GMT
Connection: keep-alive
With Helmet:
HTTP/1.1 200 OK
X-DNS-Prefetch-Control: off
X-Frame-Options: SAMEORIGIN
Strict-Transport-Security: max-age=15552000; includeSubDomains
X-Download-Options: noopen
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Content-Type: text/html; charset=utf-8
Content-Length: 12
ETag: W/"c-Lve95gjOVATpfV8EL5X4nxwjKHE"
Date: Wed, 08 Apr 2020 07:01:18 GMT
Connection: keep-alive
Extended rules
As we are going to support Adobe products (reader, flash...) and we will use HTTPS. We will extend the Helmet rules.
Optional: you need to have a report URL in order to monitor the violations of Expect Certificate Transparency performed by old browsers
Headers in scope
- ✅ Include Basic Setup headers too
- ✅ Cross Domain Policies
- ✅ Expect Certificate Transparency
Code
const helmet = require('helmet')
app.use(helmet())
// Sets Expect-CT: enforce; max-age=30;
app.use(helmet.expectCt({
enforce: true,
maxAge: 30 // 30 minutes
}))
// Sets "X-Permitted-Cross-Domain-Policies: none"
app.use(helmet.permittedCrossDomainPolicies())
Response headers
HTTP/1.1 200 OK
X-DNS-Prefetch-Control: off
X-Frame-Options: SAMEORIGIN
Strict-Transport-Security: max-age=15552000; includeSubDomains
X-Download-Options: noopen
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Expect-CT: enforce, max-age=30
X-Permitted-Cross-Domain-Policies: none
Content-Type: text/html; charset=utf-8
Content-Length: 12
ETag: W/"c-Lve95gjOVATpfV8EL5X4nxwjKHE"
Date: Wed, 08 Apr 2020 07:21:28 GMT
Connection: keep-alive
Evolutionary rules
The most powerful headers for security are Feature Policy and Content Security Policy as they will define what content source (images, js, css..) or features (xhr, geolocation, iframes...) is allowed to be part of our application.
For sure these headers are going to evolve with our project requirements. By default we will lock down all the content sources and features in order to progressively open them.
A a best practice, please include violation routes in order to track any potential issue.
Headers in scope
- ✅ Include Basic too...
- ✅ Content Security Policy
- ✅ Feature Policy
- ✅ Referrer Policy
A great starter policy for CSP allows images, scripts, AJAX, form actions, and CSS from the same origin, and does not allow any other resources to load (eg object, frame, media, etc). It is a good starting point for many sites.
Code
const helmet = require('helmet')
app.use(helmet())
// Sets Expect-CT: enforce; max-age=30;
app.use(helmet.expectCt({
enforce: true,
maxAge: 30 // 30 minutes
}))
// Sets "X-Permitted-Cross-Domain-Policies: none"
app.use(helmet.permittedCrossDomainPolicies())
// Sets "Referrer-Policy: same-origin".
app.use(helmet.referrerPolicy({ policy: 'same-origin' }))
// Sets "X-Frame-Options: DENY"
app.use(helmet.frameguard({ action: 'deny' }))
/* Sets "default-src: 'none';
script-src 'self';
connect-src 'self';
img-src 'self';
style-src 'self';
base-uri 'self';
form-action 'self'"
*/
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'none'"],
scriptSrc: ["'self'"],
connectSrc: ["'self'"],
imgSrc: ["'self'"],
styleSrc: ["'self'"],
baseUri: ["'self'"],
formAction: ["'self'"],
reportUri: '/report-csp-violation',
upgradeInsecureRequests: true,
workerSrc: false
}
}))
/*
Sets "Feature-Policy: sync-script 'self';
sync-xhr 'self';
accelerometer 'none';
ambient-light-sensor 'none';
autoplay 'none';
camera 'none';
document-domain 'none';
document-write 'none';
encrypted-media 'none';
font-display-late-swap 'none';
fullscreen 'none';
geolocation 'none';
gyroscope 'none';
layout-animations 'none';
legacy-image-formats 'none';
loading-frame-default-eager 'none';
magnetometer 'none';
microphone 'none';
midi 'none';
oversized-images 'none';
payment 'none';
picture-in-picture 'none';
serial 'none';speaker 'none';
unoptimized-images 'none';
unoptimized-lossless-images 'none';
unoptimized-lossy-images 'none';
unsized-media 'none';usb 'none';
vertical-scroll 'none';
vibrate 'none';
vr 'none';
wake-lock 'none';
xr 'none'"
*/
app.use(helmet.featurePolicy({
features: {
syncScript: ["'self'"],
syncXhr: ["'self'"],
accelerometer: ["'none'"],
ambientLightSensor: ["'none'"],
autoplay: ["'none'"],
camera: ["'none'"],
documentDomain: ["'none'"],
documentWrite: ["'none'"],
encryptedMedia: ["'none'"],
fontDisplayLateSwap: ["'none'"],
fullscreen: ["'none'"],
geolocation: ["'none'"],
gyroscope: ["'none'"],
layoutAnimations: ["'none'"],
legacyImageFormats: ["'none'"],
loadingFrameDefaultEager: ["'none'"],
magnetometer: ["'none'"],
microphone: ["'none'"],
midi: ["'none'"],
oversizedImages: ["'none'"],
payment: ["'none'"],
pictureInPicture: ["'none'"],
serial: ["'none'"],
speaker: ["'none'"],
unoptimizedImages: ["'none'"],
unoptimizedLosslessImages: ["'none'"],
unoptimizedLossyImages: ["'none'"],
unsizedMedia: ["'none'"],
usb: ["'none'"],
verticalScroll: ["'none'"],
vibrate: ["'none'"],
vr: ["'none'"],
wakeLock: ["'none'"],
xr: ["'none'"]
}
}))
Response headers
Connection: keep-alive
Content-Length: 12
Content-Security-Policy: default-src 'none'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self'; base-uri 'self'; form-action 'self'; report-uri /report-csp-violation; upgrade-insecure-requests
Content-Type: text/html; charset=utf-8
Date: Wed, 08 Apr 2020 07:52:55 GMT
ETag: W/"c-Lve95gjOVATpfV8EL5X4nxwjKHE"
Expect-CT: enforce, max-age=30
Feature-Policy: sync-script 'self';sync-xhr 'self';accelerometer 'none';ambient-light-sensor 'none';autoplay 'none';camera 'none';document-domain 'none';document-write 'none';encrypted-media 'none';font-display-late-swap 'none';fullscreen 'none';geolocation 'none';gyroscope 'none';layout-animations 'none';legacy-image-formats 'none';loading-frame-default-eager 'none';magnetometer 'none';microphone 'none';midi 'none';oversized-images 'none';payment 'none';picture-in-picture 'none';serial 'none';speaker 'none';unoptimized-images 'none';unoptimized-lossless-images 'none';unoptimized-lossy-images 'none';unsized-media 'none';usb 'none';vertical-scroll 'none';vibrate 'none';vr 'none';wake-lock 'none';xr 'none'
Strict-Transport-Security: max-age=15552000; includeSubDomains
Referrer-Policy: same-origin
X-Content-Type-Options: nosniff
X-DNS-Prefetch-Control: off
X-Download-Options: noopen
X-Frame-Options: DENY
X-Permitted-Cross-Domain-Policies: none
X-XSS-Protection: 1; mode=block
CSP Violations
Note: If you’re using a CSRF module like csurf, you might have problems handling these violations without a valid CSRF token. The fix is to put your CSP report route above csurf middleware.
// You need a JSON parser first.
app.use(bodyParser.json({
type: ['json', 'application/csp-report']
}))
app.post('/report-csp-violation', (req, res) => {
if (req.body) {
console.log('CSP Violation: ', req.body)
} else {
console.log('CSP Violation: No data received!')
}
res.status(204).end()
})
Implementation Strategies
Using aggressive policies for CSP can be very very challenging. This is why is very important to have a clear implementation strategy.
Weapp from Scratch
If you are building a site from scratch you can always follow the lock down strategy and open the policies as soon as you got rules violations an you have a clear business reason, like enable a CDN to load your Images but not CSS or JS.
Webapp refactor
If there is a running platform, the best approach is to collect data before implementing any real policy. Use the reportOnly: true
setup in order to get all the violations without affecting the current users.
This will let you a lot of time to understand the current dependencies and validate the rules based on a product requirements.
Other headers
There are other relevant headers that aren't part of Helmet.
Refs:
- Helmet | Docs
- JSConf EU 2019 | Stefan Judis | HTTP headers for the responsible developer
- OWASP | Johan Rydberg Möller | Practical HTTP Header Attacks