Safe CSP with Angular
The Content Security Policy is a special HTTP security header that instructs the browser to limit the execution of e.g. JavaScript or CSS to well-known sources like the own host.The main concern is the danger of injection attacks via inline code. Hence, application that use inline code and want to actually use the security benefits of this header have to either provide the hash of the inline code or a nonce.Providing the hash is cumbersome, so we focus on the nonce approach. The nonce is a random, dynamically per-request changing value that is provided in the CSP header as well as a special attribute for the relevant inline code fragments.When these nonces match, the browser allows the execution.
Originally this blog post contained some guidance how to workaround Angular being reliant on inline styles and the lack of support for setting CSP nonces in Angularwhich has been tracked in an issue with a long history since 2018. With Angular 16 this has nowbeen fixed in 2023. Hence, this blog post has been rewritten to show the current state while keeping the legacy workaround moved to the end for completeness.
Step 1: Add the nonce
This is described in the upstream documentation. Just as an example for what I use:
@NgModule({
// ...
providers: [{ provide:CSP_NONCE, useValue: "random_csp_nonce"}],
bootstrap: [AppComponent]
})
export class AppModule { }
This now adds to every inline style-src generated by Angular the nonce "random_csp_nonce".Using this static nonce would not bring any benefit. An attacker can just use the well-known public nonce.
Step 2: Replace the nonce two-times
We will replace the nonce-two-times:
- The first replacement occurs during the deployment and statically replaces "random_csp_nonce" by a non-guessable, secret placeholder.
- The second replacement happens per request and replaces the placeholder by a random nonce. I personally use Kubernetes and therefore will quickly show how this can be done there via Helm.
Replace the unsafe placeholder
We just use sed to replace the placeholder in the modified shared_styles_host.ts which ends up as part of the main.[...].js bundled by the Angular build process.Here I defined a ConfigMap which is mounted in the nginx-container that actually serves the app and acts as new entrypoint:
apiVersion: v1 kind: ConfigMap metadata: name: homepage-nonce-entrypoint data: nonce-entrypoint.sh: | #! /bin/sh sed -i 's/random_csp_nonce/{{ .Values.nonce }}/g' /usr/share/nginx/html/main.*.js ./docker-entrypoint.sh nginx -g "daemon off;"
The replacement value for the fixed value "random_csp_nonce" should be secure. I personally use:
helm install homepage . -f values.yml --set nonce=$(pwgen 64)
Replace the new placeholder with the random nonce (websrv)
A new variant using websrv, a small webserver I implemented as a toy project in golang.This uses a short-lived Session-Cookie to set the CSP-Header and is therefore not mangled with the TLS layer (see alternative in next subsection).
./websrv -config-file config.yaml [target-path]
And then in the config.yaml
:
headers: Content-Security-Policy: default-src 'self'; frame-ancestors 'none'; form-action 'none'; font-src 'self'; img-src 'self'; script-src 'self'; style-src 'self' 'nonce-random_csp_nonce'; worker-src 'self' angularcsp: enabled: true filepath: ^/main.*\.js$ variable: random_csp_nonce sessioncookie: name: Nonce-Id maxage: 600
Replace the new placeholder with the random nonce (nginx)
To actually get a safe nonce we can use the ssl_session_id as pointed out by Paweł Krawczyk.Here the relevant part of the nginx-inc ingress definition. We also have to serve the CSP HTTP header via this Ingress so that CSP header and style nonce match.
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: homepage annotations: nginx.org/location-snippets: | sub_filter_once off; sub_filter_types text/javascript application/javascript; sub_filter '{{ $.Values.nonce }}' '$ssl_session_id'; add_header Content-Security-Policy "default-src 'self'; frame-ancestors 'none'; form-action 'none'; font-src 'self'; img-src 'self'; script-src 'self'; style-src 'self' 'nonce-$ssl_session_id'; worker-src 'self'" always;
Step 1: Add the nonce (Angular<16 version)
This is just a variant of Step 1 for Angular versions < 16.
Ferdie Sletering pointed in his blog post out, how the nonce can be introduced.First we copy the shared_style_host.ts from the Angular sources and apply the following diff:
--- shared_style_host.ts 2021-10-09 19:46:12.972338484 +0200 +++ shared_style_host_nonce.ts 2021-10-09 19:49:45.229080744 +0200 @@ -46,6 +46,7 @@ styles.forEach((style: string) => { const styleEl = this._doc.createElement('style'); styleEl.textContent = style; + styleEl.setAttribute('nonce', 'random_csp_nonce'); // Add nonce styleNodes.push(host.appendChild(styleEl)); }; }
The modified class can than be served as a replacement for the original Angular shared_styles_host by defining a module like:
@NgModule({
providers: [
{ provide: ɵDomSharedStylesHost, useClass: NonceDomSharedStylesHost },
],
})
export class StyleNonceModule { }
This now adds to every inline style-src generated by Angular the nonce "random_csp_nonce".Using this static nonce would not bring any benefit. An attacker can just use the well-known public nonce.