Error

Introduction

During a recent security review, our team flagged a critical issue in our Content Security Policy (CSP): the use of 'unsafe-inline'.

A CSP is an HTTP header that tells the browser which resources (scripts, styles, images, etc.) are allowed to load. It is one of the strongest defenses against Cross-Site Scripting (XSS) and other injection attacks. However, the directive 'unsafe-inline' weakens this protection by allowing any inline JavaScript or CSS to run. While convenient during development, it poses a serious vulnerability in production and is commonly flagged by security teams.

When we removed 'unsafe-inline', the application immediately broke — Angular scripts failed, inline styles were blocked, and third-party libraries like Font Awesome and Flatpickr stopped working.

Our setup is a standalone Angular frontend and a FastAPI backend, both deployed in the same App Service. The backend serves the Angular app by routing all wildcard paths (/*) to index.html. With both UI and API sharing the same deployment, CSP changes had to be applied carefully to avoid breaking either layer.

This post explains how we solved these issues step by step to achieve full CSP compliance while keeping the application fully functional.

Problem Statement

Removing 'unsafe-inline' caused multiple issues:

  • Angular scripts and styles failed – Angular depends on certain inline initializations.
  • Third-party libraries broke – Font Awesome and Flatpickr injected CSS blocked by CSP.
  • Inline styles were blocked – Angular-generated styles needed explicit allowance.
  • Build conflicts – Angular’s default CSS optimizations didn’t play well with a strict CSP.

Keeping 'unsafe-inline' was not an option. The challenge was to make our app secure without losing functionality, and to ensure the fix was maintainable long-term.

The Original CSP Policy

Here’s what our CSP looked like before the fix (with client-specific domains/CDNs removed for clarity):

response.headers["Content-Security-Policy"] = (
    "default-src 'self'; "
    "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
    "connect-src 'self'; "
    "font-src 'self' https://fonts.gstatic.com; "
    "img-src 'self' https://cdn.jsdelivr.net; "
    "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; "
    "frame-ancestors 'self'; "
    "frame-src 'self'; "
    "form-action 'self';"
)

This worked in development but had serious drawbacks:

  • It depended on 'unsafe-inline', which defeats CSP’s core protections.
  • It wasn’t strict enough for production security.
  • Our security team marked it as a critical vulnerability.

This was the baseline we had to improve.

Why This Policy Was Insecure

On paper, the CSP looked restrictive: most sources were locked to 'self' with a few exceptions (Google Fonts, jsDelivr). But 'unsafe-inline' completely undermined it.

  • Inline scripts or styles could run unchecked, leaving the app open to XSS.
  • No nonces or hashes were used to whitelist trusted inline code.
  • The CSP gave a false sense of security: restrictive in appearance, permissive in practice.

This is why we had to redesign the policy to remove 'unsafe-inline' while keeping the Angular + FastAPI app functional.

Solution

Fixing our CSP wasn’t just about removing 'unsafe-inline'. We had to carefully update both the Angular build and third-party integrations so the app would still run under a strict policy. Here’s what we did:

1. Removed 'unsafe-inline'

The first step was to remove 'unsafe-inline' from the CSP header. This was necessary for security, but it immediately broke the app:

  • Angular scripts and styles stopped working.
  • Third-party libraries like Font Awesome and Angular Material failed to load inline CSS or initialization scripts.
  • Multiple console errors appeared, e.g.:
Refused to execute inline event handler because it violates the following Content Security Policy directive: 
"script-src 'self' ". 
Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-...') is required to enable inline execution. 
Note that hashes do not apply to event handlers, style attributes and javascript: navigations unless the 'unsafe-hashes' keyword is present.

This confirmed that simply removing 'unsafe-inline' broke functionality, so we needed a way to allow trusted inline code securely.

2. Introduced Nonces

To replace 'unsafe-inline' safely, we added nonces. A nonce is a random value generated per request, included in the CSP header, and injected into inline <script> and <style> tags. Only code with the matching nonce is executed by the browser.

import secrets

# Generate a random nonce for each response
nonce = secrets.token_urlsafe(16)

The corresponding CSP header now uses the nonce instead of 'unsafe-inline':

response.headers["Content-Security-Policy"] = (
    "default-src 'self'; "
    f"script-src 'self' 'nonce-{nonce}' https://cdn.jsdelivr.net; "
    "connect-src 'self'; "
    "font-src 'self' https://fonts.gstatic.com; "
    "img-src 'self' https://cdn.jsdelivr.net; "
    f"style-src 'self' 'nonce-{nonce}' https://fonts.googleapis.com; "
    "frame-ancestors 'self'; "
    "frame-src 'self'; "
    "form-action 'self';"
)

In our setup, the UI’s index.html is served by the FastAPI backend via a wildcard route (/*), so we inject the nonce dynamically when returning the page. We also attach it to the <app-root> element for Angular:

# The same nonce needs to be attached.
# Store the value in request object / global etc to pass it around.
# Attach ngCspNonce to <app-root>
content = re.sub(
    r'<app-root(?![^>]*\bngCspNonce=)([^>]*)>',
    fr'<app-root\1 ngCspNonce="{nonce}">',
    content,
)

# Attach nonce to all inline <script> and <style> tags
content = re.sub(
    r'(<script(?![^>]*\bnonce=))',
    fr'\1 nonce="{nonce}"',
    content
)
content = re.sub(
    r'(<style(?![^>]*\bnonce=))',
    fr'\1 nonce="{nonce}"',
    content
)

Key points:

Using the wildcard route for index.html allows this injection to happen on every response, ensuring the frontend always receives a valid nonce.

'unsafe-inline' is fully removed.

Nonces are generated per request and injected dynamically into <app-root>, inline <script>, and <style> tags.

Angular, Angular Material, and other libraries can safely run inline code without breaking CSP.

3. Updated Angular Build Configuration

After introducing nonces in Step 2, most CSP-related errors disappeared, reducing console violations from around 15 to just 2. However, a couple of issues remained:

  1. Critical CSS inlining in Angular’s default build was still causing CSP violations.
  2. Font Awesome, which we use for icons, automatically injects CSS at runtime. This violates CSP because the styles are inline.

To resolve these issues, we made the following updates:

a) Adjust Angular Build

We updated angular.json (indside the build object) to disable inlined critical CSS while keeping other optimizations:

"optimization": {
  "scripts": true,
  "styles": {
    "minify": true,
    "inlineCritical": false
  },
  "fonts": true
}

This change:

  • Prevents inline critical CSS from violating CSP.
  • Ensures all styles are bundled as external files compatible with nonces or trusted sources.
  • Maintains minification and font optimizations.

b) Disable Font Awesome Auto CSS Injection

Font Awesome injects CSS at runtime, which breaks a strict CSP. Since we use Font Awesome for icons, we disabled this behavior in app.config.ts:

import { config } from '@fortawesome/fontawesome-svg-core';
config.autoAddCss = false;

With this, Font Awesome CSS must be loaded manually via trusted external files, ensuring compliance with CSP.

After these changes, all remaining CSP errors were resolved except for a small inline CSS from Flatpickr, which we handled in the next step.

4. Handling Flatpickr CSS with a Style Hash

After updating the Angular build and fixing Font Awesome, all CSP errors were resolved except for one: inline styles injected by Flatpickr. These styles cannot use nonces because they are generated at runtime.

The browser console helped here by providing the exact SHA-256 hash of the blocked CSS in the CSP violation message. We then added this hash to the style-src directive of the CSP header to allow only this specific inline style to execute:

# Example SHA-256 hash for Flatpickr CSS
flatpickr_hash = "'sha256-t4I2teZN5ZH+VM+XOiWlaPbsjQHe+k9d6viXPpKpNWA='"

response.headers["Content-Security-Policy"] = (
    "default-src 'self'; "
    f"script-src 'self' 'nonce-{nonce}' https://cdn.jsdelivr.net; "
    "connect-src 'self'; "
    "font-src 'self' https://fonts.gstatic.com; "
    "img-src 'self' https://cdn.jsdelivr.net; "
    f"style-src 'self' 'nonce-{nonce}' https://fonts.googleapis.com {flatpickr_hash}; "
    "frame-ancestors 'self'; "
    "frame-src 'self'; "
    "form-action 'self';"
)

Key points:

  • The browser error provided the exact hash, allowing us to whitelist only that CSS.
  • This approach keeps the CSP strict and secure, without reintroducing 'unsafe-inline'.
  • After this final step, all scripts and styles — including Angular, Angular Material, Font Awesome, and Flatpickr — worked correctly under a strict CSP.

This completes the migration from a permissive, unsafe CSP to a secure, nonce- and hash-based policy without breaking application functionality.

Conclusion

Migrating from a permissive CSP with 'unsafe-inline' to a strict, secure policy required careful adjustments across both the Angular frontend and FastAPI backend. By removing 'unsafe-inline', introducing nonces, updating the Angular build configuration, handling Font Awesome CSS, and whitelisting runtime-generated Flatpickr styles with a hash, we achieved full CSP compliance without breaking the application.

Key takeaways:

  • Nonces allow trusted inline scripts and styles to run while blocking everything else.
  • Build configuration matters — Angular’s default critical CSS inlining can conflict with CSP.
  • Third-party libraries like Font Awesome and Flatpickr often require special handling.
  • Browser-provided hashes can safely whitelist specific inline CSS when nonces aren’t possible.

With this approach, the application is now secure against Cross-Site Scripting (XSS) attacks, fully functional, and production-ready. This workflow provides a practical blueprint for developers aiming to enforce strict CSPs in Angular + FastAPI applications.