/ โ† Security Audit Fixes / CSP in Spring, Tomcat, Angular
In this guide
  1. What a Content-Security-Policy header does in a Java or Angular app
  2. How to check what your app sends now
  3. How to set the Content Security Policy header in Spring Boot
  4. How to set the Content Security Policy header in Tomcat 9
  5. How to set the Content Security Policy header in Angular
  6. A worked example: Example Bakery on Spring Boot and Angular
  7. The fix, step by step
  8. Edge cases worth knowing
  9. Keeping the policy working
  10. Questions and answers

What a Content-Security-Policy header does in a Java or Angular app

A Content Security Policy is an HTTP response header that tells the browser which sources it may load scripts, styles, images, fonts, frames and connections from. If an attacker manages to inject a script tag into one of your pages, a good policy stops the browser from running it. That makes CSP one of the most useful defences against cross-site scripting, and it is one of the headers every security scanner checks for.

The header itself is the same whatever framework you use. A sensible starting policy looks like this:

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'

What changes between Spring Boot, Tomcat 9 and Angular is where the header is added. In a Spring Boot application the code that builds the response can add it. In a plain Tomcat 9 deployment you add it with a servlet filter. An Angular app is different again: Angular runs in the browser, so it cannot send HTTP headers at all. The server that delivers the built files, whether that is nginx, Apache, a Node server or Spring Boot itself, has to send the header for it.

StackWhere the header is setBuilt-in support
Spring Boot with Spring SecurityThe SecurityFilterChain beanYes, but no policy is sent until you write one
Spring Boot without Spring SecurityA servlet filter beanNo, add a small filter
Tomcat 9 (any WAR)A filter declared in web.xml, or the proxy in frontNo, the built-in security filter does not set CSP
AngularThe web server or CDN that serves index.htmlNonce and hash helpers, but the header comes from the server

If you are not sure which headers your site sends today, the missing security headers guide explains what each one does and how scanners grade them.

How to check what your app sends now

Before changing code, see what actually reaches the browser. Headers can be added or removed by the application, the servlet container, a reverse proxy and a CDN, so test the public address rather than localhost:

curl -sI https://example.com/ | grep -i content-security
curl -sI https://example.com/app/dashboard | grep -i content-security

Check more than one URL. A policy set only on the home page, or only on HTML responses from one controller, leaves the rest of the site unprotected. Also check that the header appears once. Two Content-Security-Policy headers are not merged: the browser enforces both, so a resource has to pass every policy, and the stricter one effectively wins.

In Chrome DevTools, open the Network panel, reload, click the document request and look under Response Headers. Then open the Console. Every blocked resource produces a message that names the directive, for example:

Refused to load the script 'https://cdn.example.net/widget.js' because it violates
the following Content Security Policy directive: "script-src 'self'".

Those messages are your to-do list. Each one is either something you want to allow, by adding its source to the right directive, or something that should not be there at all.

๐Ÿ’ก Start every new policy as Content-Security-Policy-Report-Only. The browser logs the same violations but blocks nothing, so you can collect the full list from real pages before you enforce it. The report-only guide covers this rollout in detail.

How to set the Content Security Policy header in Spring Boot

If your application uses Spring Security, the headers configuration is the right place. Spring Security already sends several security headers by default, but it does not send a Content Security Policy until you supply one, because no single policy suits every application. In Spring Security 6 and Spring Boot 3, add it to your filter chain bean:

@Configuration
public class SecurityConfig {

  @Bean
  SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.headers(headers -> headers
        .contentSecurityPolicy(csp -> csp
            .policyDirectives("default-src 'self'; script-src 'self'; "
                + "style-src 'self'; img-src 'self' data:; object-src 'none'; "
                + "base-uri 'self'; frame-ancestors 'self'")
        )
    );
    return http.build();
  }
}

To trial the policy first, add .reportOnly() after policyDirectives(...). Spring then sends Content-Security-Policy-Report-Only instead, and you remove that one call when you are ready to enforce.

Spring Boot without Spring Security

If the app does not use Spring Security, register a small filter. Spring Boot picks up any Filter bean and applies it to every request:

@Component
public class CspHeaderFilter extends OncePerRequestFilter {

  private static final String POLICY =
      "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'";

  @Override
  protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
                                  FilterChain chain) throws ServletException, IOException {
    res.setHeader("Content-Security-Policy", POLICY);
    chain.doFilter(req, res);
  }
}

Nonces in Thymeleaf templates

If your server-rendered pages need inline scripts, give each response a fresh random nonce instead of allowing 'unsafe-inline'. Generate at least 128 bits with SecureRandom, Base64-encode it, put it in the header as script-src 'self' 'nonce-VALUE', store it as a request attribute, and print it in the template:

<script th:attr="nonce=${cspNonce}">
  window.appConfig = { region: 'uk' };
</script>

The nonce must change on every response. A fixed value, or one cached in a full-page cache, gives an attacker the same key you use and defeats the point.

How to set the Content Security Policy header in Tomcat 9

Tomcat 9 ships with HttpHeaderSecurityFilter, which can add HSTS, X-Frame-Options and X-Content-Type-Options, but it has no Content Security Policy option. For a WAR running on Tomcat 9 you have three choices: a filter inside your application, a filter that applies to every application on the server, or the reverse proxy in front of Tomcat.

A filter class

Tomcat 9 implements Servlet 4.0, so use the javax.servlet package. Tomcat 10 and later moved to jakarta.servlet, and a class compiled against the wrong one will not load.

package com.example.security;

import java.io.IOException;
import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;

public class CspFilter implements Filter {
  private String policy;

  @Override
  public void init(FilterConfig config) {
    policy = config.getInitParameter("policy");
  }

  @Override
  public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
    ((HttpServletResponse) res).setHeader("Content-Security-Policy", policy);
    chain.doFilter(req, res);
  }
}

Registering it in web.xml

Declare it in your application's WEB-INF/web.xml. Putting it in Tomcat's own conf/web.xml applies it to every application, but the class must then be in Tomcat's lib folder.

<filter>
  <filter-name>cspFilter</filter-name>
  <filter-class>com.example.security.CspFilter</filter-class>
  <init-param>
    <param-name>policy</param-name>
    <param-value>default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'</param-value>
  </init-param>
</filter>
<filter-mapping>
  <filter-name>cspFilter</filter-name>
  <url-pattern>/*</url-pattern>
  <dispatcher>REQUEST</dispatcher>
  <dispatcher>ERROR</dispatcher>
</filter-mapping>

The ERROR dispatcher means Tomcat's error pages carry the header too. Restart the application or Tomcat, then run the curl check again.

If Tomcat sits behind Apache or nginx, setting the header on the proxy is often simpler and covers static files that never reach Tomcat. The Apache and nginx security header guides show the syntax. Set it in one place only, or you will send two policies.

How to set the Content Security Policy header in Angular

An Angular build is a folder of static files: index.html, JavaScript bundles and CSS. The header has to be added by whatever serves that folder. Two Angular behaviours decide what the policy must allow.

Giving Angular a nonce

Since Angular 16 you can put an ngCspNonce attribute on the root element. Angular then adds that nonce to every style element it creates:

<app-root ngCspNonce="NGINX_CSP_NONCE"></app-root>

The server replaces the placeholder with a fresh random value on every response and sends the same value in the header. With nginx serving the build:

location / {
  root /var/www/example-app/browser;
  try_files $uri /index.html;
  sub_filter_once off;
  sub_filter 'NGINX_CSP_NONCE' $request_id;
  add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-$request_id'; style-src 'self' 'nonce-$request_id'; object-src 'none'; base-uri 'self'" always;
}

The $request_id variable is a random 32-character value generated for each request, which is enough for a nonce. Make sure nothing in front, such as a CDN, caches index.html, or every visitor receives the same nonce.

Hashes instead of nonces

If your hosting cannot rewrite index.html per request, use hashes. Recent Angular versions, from 19 onwards, offer an automatic option in angular.json that computes hashes for the inline scripts in index.html and writes a policy into a meta tag:

"build": {
  "options": {
    "security": { "autoCsp": true }
  }
}

A meta-tag policy cannot use frame-ancestors or reporting, so still send a header from the server for those. While you develop, ng serve accepts a headers object in its options, which lets you test the same policy locally.

A worked example: Example Bakery on Spring Boot and Angular

Example Bakery runs an Angular storefront at shop.example.com, served by nginx, and a Spring Boot API at api.example.com. A scan reports no Content Security Policy on either.

  1. They add a report-only policy to the API's SecurityFilterChain with default-src 'none'; frame-ancestors 'none'. An API that returns only JSON loads nothing, so the strictest policy is safe.
  2. For the storefront they add the nginx block above as Content-Security-Policy-Report-Only and the ngCspNonce placeholder in index.html.
  3. The Console shows three kinds of message: the API calls are refused by connect-src, a payment script from the provider's domain is refused by script-src, and product photos from images.example.com are refused by img-src.
  4. They add connect-src 'self' https://api.example.com, the payment domain to script-src and frame-src, and https://images.example.com to img-src.
  5. After a week with no new violations from real visitors, they change the header name to Content-Security-Policy.

The finished storefront policy is longer than the starter, but every source in it is one they recognise and chose.

The fix, step by step

Step 1

Record the headers you send now

Run curl -sI against the live home page, an app page and a missing URL, and note any Content-Security-Policy lines. Open Chrome DevTools on the same pages and check the Response Headers for the document. Write down who sets each header today, whether the application, Tomcat, a proxy or a CDN. You are done when you know whether a policy exists, where it comes from and whether it appears more than once.

Step 2

Choose one place to own the policy

Decide which layer will add the header: the Spring SecurityFilterChain, a servlet filter in Tomcat 9, or the nginx or Apache server that serves your Angular build. Remove any other copy so the browser only ever receives one policy. If a CDN adds security headers, switch that rule off or make it the single owner. Confirm with curl that, after this change, at most one policy header is returned.

Step 3

Write a starter policy

Begin with default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self' and add img-src 'self' data: if your pages use inline data images. Keep it as one string in a constant or configuration value. Avoid 'unsafe-inline' and wildcards at this stage, because you want the report to show you everything the page loads. Check the string has no stray line breaks and that every keyword is in single quotes.

Step 4

Add it in report-only mode

In Spring Security add .reportOnly() after policyDirectives; in a Tomcat filter or on nginx use the header name Content-Security-Policy-Report-Only. Deploy to a staging copy first if you have one. Reload a few pages and run curl again. You should see the report-only header name, the exact policy string you wrote and no change at all to how the site looks or behaves.

Step 5

Collect and sort the violations

Browse every page template with the Console open: home, listing, product or article, account, checkout and any embedded content. Each message names a directive and a blocked source. Sort them into sources you trust and want, such as your API or payment provider, and ones you do not recognise. Add a reporting endpoint so real visitors' browsers send you violations you would not see yourself.

Step 6

Allow the sources you need

Add each trusted host to the narrowest directive that fits: APIs to connect-src, script hosts to script-src, image hosts to img-src, embedded frames to frame-src. For inline scripts or Angular styles, add a per-request nonce rather than 'unsafe-inline'. Redeploy and repeat the browse until the Console stays quiet on every template you tested, and keep a note of why each new source was added so it can be reviewed later.

Step 7

Handle Angular nonces or hashes

For Angular, add ngCspNonce to the root element with a placeholder, replace it per request on the server and send the same value in the header. If the hosting cannot do that, enable hash-based output in angular.json and keep a server header for frame-ancestors. Check that index.html is not cached by a CDN, then reload twice and confirm the nonce changes between responses.

Step 8

Switch to enforcement

When a few days of real traffic produce no new violations you intend to allow, change the header name to Content-Security-Policy, or remove .reportOnly() in Spring Security. Keep the reporting directive in the policy. Test the main user journeys, especially login, forms and payment, straight after the release. Confirm with curl that the enforcing header name is now returned on every URL you tested earlier.

Step 9

Re-scan and keep a watch

Run a security header scan of the live site and confirm the policy is detected and graded. Add the curl check to your deployment pipeline so a missing or duplicated header fails the build. Review the violation reports weekly for the first month, then monthly. Each time someone adds a new third-party tool, update the policy in the same change rather than afterwards.

Edge cases worth knowing

Keeping the policy working

A Content Security Policy is not a set-and-forget header. Every new analytics tag, chat widget, font host or embedded video needs a matching source, and a developer under time pressure is tempted to add 'unsafe-inline' or a wildcard to make the error go away.

The security audit guide explains how CSP fits alongside the other headers that scanners check.

Questions and answers

1. How do I set a Content Security Policy header in Spring Boot?

In Spring Boot with Spring Security, you set it in your SecurityFilterChain bean by calling headers, then contentSecurityPolicy, and passing your policy string to policyDirectives. Spring Security sends several headers by default but never a CSP until you write one, because every application loads different sources. If the project does not use Spring Security, register a small filter bean that calls setHeader on each response. In both cases, test the live site with curl afterwards, because a proxy or CDN in front can add, remove or duplicate the header.

2. How do I set a Content Security Policy header in Tomcat 9?

In Tomcat 9 you add a Content Security Policy with a servlet filter, because the built-in HttpHeaderSecurityFilter covers HSTS and some other headers but has no CSP option. Write a filter class using the javax.servlet package, read the policy from an init parameter and set it on every response. Register the filter in your application's web.xml with a url-pattern of slash star. If Tomcat runs behind Apache or nginx, setting the header there instead is often simpler, as long as only one layer sends it.

3. How do I set a Content Security Policy header in Angular?

You set it on the web server or CDN that serves the Angular build, because Angular runs in the browser and cannot send HTTP headers. Configure nginx, Apache, a Node server or your hosting platform to add the header to index.html and the other files. Angular injects component styles as inline style elements, so either allow them with a nonce, using the ngCspNonce attribute on the root element, or with unsafe-inline for styles. Recent versions can also produce hash-based policies automatically from angular.json.

4. Does Spring Security add a Content Security Policy by default?

No, Spring Security does not add a Content Security Policy by default. Its default headers include cache control, X-Content-Type-Options, X-Frame-Options and, over HTTPS, Strict-Transport-Security, but CSP is left out because a policy that suits one application would break another. You have to supply the directives yourself through the headers configuration. You can call reportOnly to send it as Content-Security-Policy-Report-Only first, which lets you see what would be blocked before anything is actually blocked for visitors.

5. Can I put the Content Security Policy in a meta tag instead?

Yes, most of a Content Security Policy can go in a meta tag with http-equiv set to Content-Security-Policy, but it is a weaker option than a header. Browsers ignore frame-ancestors, report-uri and sandbox in a meta tag, and the policy only applies from the point the tag is read, so anything earlier in the head is not covered. Report-only mode is not available in a meta tag either. Many security scanners only read response headers and will still report the policy as missing.

6. Why does my Angular app break after adding a CSP?

Most Angular breakages after adding a CSP come from inline styles being blocked. Angular adds each component's styles as a style element in the page, which a style-src of self alone does not allow. Add a nonce through ngCspNonce, or allow unsafe-inline for styles only as a stopgap. Other common causes are API calls blocked by connect-src, fonts from another domain blocked by font-src, and the critical CSS loader's onload attribute. The browser Console names the directive for each blocked item.

7. What is the difference between a nonce and a hash in CSP?

A nonce is a random value the server generates for each response and places in both the header and the script or style tag, while a hash is a fingerprint of the exact contents of an inline block. Nonces suit pages built per request, such as Spring templates or an nginx-rewritten Angular index file. Hashes suit static files, because the value only changes when the code changes. Either way, avoid unsafe-inline, which allows every inline script, including any an attacker manages to inject.

Check your Content Security Policy header

Run the security audit on aiwebpageseo.com to see whether each page sends a Content Security Policy, whether it is duplicated, and which other security headers are missing.

Open Security Audit Fixes โ†’
๐Ÿ’ฌ Got a problem?
Related Guides: All Security Audit Fixes  ·  Fix missing security headers  ·  Test a CSP with report-only  ·  Security headers on nginx  ·  Security headers on Apache  ·  Learning Hub

About aiwebpageseo

aiwebpageseo.com is a data-driven SEO and AEO (Answer Engine Optimisation) platform providing a free suite of technical SEO tools. Rather than relying on AI-theorised assumptions, the platform analyses live URL performance, delivering objective diagnostics, page speed metrics, CLS debugging, and site crawl data alongside actionable technical tutorials.