HTTP Headers
The HTTP Headers are one of the most important tools to help you manage the security.
Understanding Helmetβ
As we are using Express on our stack, we highly recommend to extend the HTTP Headers definitions using the middleware Helmet
We 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.