How to implement an inline styles Content Security Policy with Angular and Nginx

Ferdie Sletering - Jun 13 '21 - - Dev Community

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

Alt Text

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'">
Enter fullscreen mode Exit fullscreen mode

The result is that our application doesn't look the same anymore.

application

Alt Text

What happened?

Due to our CSP policy, the browser blocks all inline styling that comes from an untrusted source.

console.log

Alt Text

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>

Alt Text

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';">
Enter fullscreen mode Exit fullscreen mode

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));
    });
  }
Enter fullscreen mode Exit fullscreen mode

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));
    });
  }
Enter fullscreen mode Exit fullscreen mode

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 {}

Enter fullscreen mode Exit fullscreen mode

After applying these changes, the style tag contains a nonce.

styletag with nonce

image

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'";
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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));
    });
  }
Enter fullscreen mode Exit fullscreen mode

Each time we reload the page, a new random nonce is generated and applied to all style tags.

random nonce styletag

Alt Text

Our application looks the same as from the beginning. But now, we have applied a CSP policy :).

application

Alt Text

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.

🌎 Live demo
📝 Github

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.

. . .