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.
Top comments (9)
I've extended this solution for express static and wrote about it here medium.com/@dmelloryan11/how-to-im...
For Angular 14 + Material there's another issue with media matcher style tag injection below are the monkey patches to these injectors to provide our custom nonce
Media Matcher override
inline-styles-csp\media-matcher.ts
Updated shared styles host
inline-styles-csp\shared_styles_host.ts
Updated module
inline-styles-csp\inline-styles-csp.module.ts
thanks,it is useful for me, but how implement inject the CSP Nonce value in the inline style generated by the third-party library in angular?
Thanks a lot for this. Very nice and cool.
However, although I implemented the solution you described, I am still getting CSP errors on elements (Angular). <br> I can see the correct CSP header with the 'nonce-123' (123 is just an example of course..), and I have logged the nonce value being set on the style elements to ensure I placed the correct one. What is strange is that both Chrome and Firefox show a <style nonce> but without the nonce code itself, and the console shows CSP errors on style-src. When I try writing a dummy attribute with the same value, for the sake of troubleshooting, I can see the nonce value properly, for example: <style test="123">.</p> <p>Any idea?</p>
Thank you, this helped a lot. However, in your demo check console log, some styles are blocked.
Glad could help, one style is intentionally blocked for demo purposes.
Thank you, looks like a clear approach. What version of Angular are you working with?
As I can only find a shared_styles_hosts.js in Angular 11. Or do I look at the wrong spot?
This may be a dumb question, but what is stopping an XSS attack from grabbing the nonce value off the meta tag too?