Skip to main content

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:

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​

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​

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​

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:​