How to implement an inline styles Content Security Policy with Angular and Nginx
Ferdie Sletering
Posted on June 13, 2021
Intro
We want to make our applications as safe as possible, so we implement a content security policy(CSP) to mitigate Cross Site Scripting (XSS) attacks or Click Jacking.
The demo application contains an ngx-bootstrap toggle and a Angular Material slider component.
application
Implement the Content Security Policy(CSP)
Let's implement a CSP header. More information about CSP.
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
The result is that our application doesn't look the same anymore.
application
What happened?
Due to our CSP policy, the browser blocks all inline styling that comes from an untrusted source.
console.log
Angular Material and ngx-bootstrap styles are added with the styleUrls property. Angular will parse the component's styling and add them to the
of the page. Based on the ViewEncapsulation property, it's global(none) or scoped(emulated).<style>
How to solve
When we have control of our styling, we could place all our CSS into a separate file. Issue solved! However, we don't have control over how libraries handle their styling.
Nonce approach
Allows an inline script or CSS to execute if the script (e.g.:
<style nonce=" r@nd0m">
) tag contains a nonce attribute matching the nonce specified in the CSP header. The nonce should be a secure random string and should not be reused.
Let's add a nonce to our CPS policy and style tags, so our inline styling comes from a trusted source.
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'nonce-random-csp-nonce';">
Add nonce to style tag
When looking into the angular/platform-browser package, the following code is responsible for injecting the style tags.
shared_styles_host.ts
private _addStylesToHost(styles: Set<string>, host: Node, styleNodes: Node[]): void {
styles.forEach((style: string) => {
const styleEl = this._doc.createElement('style');
styleEl.textContent = style;
styleNodes.push(host.appendChild(styleEl));
});
}
Luckily Angular provides us with dependency providers, which allows us to create a custom _addStylesToHost function.
We copy the shared_styles_host.ts
and modify the _addStylesToHost
method.
private _addStylesToHost(
styles: Set<string>,
host: Node,
styleNodes: Node[]
): void {
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));
});
}
We create a module that can be imported in our app.module.ts
inline-styles-csp.module.ts
import { NgModule } from '@angular/core';
import { CustomDomSharedStylesHost } from './shared_styles_host';
import { ɵDomSharedStylesHost } from '@angular/platform-browser';
@NgModule({
providers: [
{ provide: ɵDomSharedStylesHost, useClass: CustomDomSharedStylesHost },
],
})
export class InlineStylesCSPModule {}
After applying these changes, the style tag contains a nonce.
styletag with nonce
We now have a static nonce that is not secure.
The nonce should be a secure random string and should not be reused.
Create a secure random string with Nginx
We use the sub_filter
module of Nginx to replace the static with a dynamic string. In that case, we use the Nginx $request_id
variable.
nginx.conf
sub_filter_once off;
sub_filter random-csp-nonce $request_id;
add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'nonce-$request_id'";
Also, note we add the add_header
to our config file.
Still, our solution doesn't work because Nginx replaces random-csp-nonce
on the index.html file. Angular adds the style tags to the document after Nginx serves the document. When we place a hard-coded <style nonce="random-csp-nonce" />
in the index.html it gets replaced with a dynamic nonce.
Add metatag
We add a new metatag to the index.html so our script can look up the dynamic nonce value.
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="CSP-NONCE" content="random-csp-nonce"/>
</head>
<body>
<app-root></app-root>
</body>
</html>
Let's update our _addStylesToHost
method to query the nonce.
private _addStylesToHost(
styles: Set<string>,
host: Node,
styleNodes: Node[]
): void {
const nonce = document
.querySelector('meta[name="CSP-NONCE"]')
?.getAttribute('content');
styles.forEach((style: string) => {
const styleEl = this._doc.createElement('style');
styleEl.textContent = style;
styleEl.setAttribute('nonce', nonce); // Add nonce
styleNodes.push(host.appendChild(styleEl));
});
}
Each time we reload the page, a new random nonce is generated and applied to all style tags.
random nonce styletag
Our application looks the same as from the beginning. But now, we have applied a CSP policy :).
application
Conclusion
Although we have proof of concept on fixing the inline styles issue, the final and more sustainable solution should come from the community and the Angular team. For now, we have to inject a custom DomSharedStylesHost class.
Demo
The Github code contains a full version of the code. However, for demo purposes, some code is stripped out of the original code.
Improvements
Nonce in style tags remains empty
The browser parses the inline styles, but the actual nonce remains empty. I don't know why this occurs. The response Content-Security-Policy header contains the correct nonce value.
Stronger nonce token
The $request_id
is not a cryptographically secure random token. We could improve this with an nginx module as Scott Helme suggests.
100% secure?
The fact we use AOT compilation means all code is already compiled and can't be tampered with. So although the code looks for the CSP header and gets the nonce, any other script that gets executed could do the same.
Any feedback or thoughts are welcome.
Posted on June 13, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
June 13, 2021