How to set a Content Security Policy header in Spring Boot, Tomcat and Angular
A Content Security Policy tells the browser which sources a page may load scripts, styles and other files from, and it is one of the strongest defences against injected scripts. Security scanners flag its absence on almost every Java and Angular site, because none of these frameworks sends one until you write it. Spring Security leaves it out by default, Tomcat 9's built-in security filter has no option for it, and Angular cannot send headers at all because it runs in the browser. This guide shows where the header belongs in each stack, gives working code for Spring Boot, a Tomcat 9 filter and an nginx server for an Angular build, explains nonces and hashes, and shows how to test the policy safely before you enforce it.
- What a Content-Security-Policy header does in a Java or Angular app
- How to check what your app sends now
- How to set the Content Security Policy header in Spring Boot
- How to set the Content Security Policy header in Tomcat 9
- How to set the Content Security Policy header in Angular
- A worked example: Example Bakery on Spring Boot and Angular
- The fix, step by step
- Edge cases worth knowing
- Keeping the policy working
- 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.
| Stack | Where the header is set | Built-in support |
|---|---|---|
| Spring Boot with Spring Security | The SecurityFilterChain bean | Yes, but no policy is sent until you write one |
| Spring Boot without Spring Security | A servlet filter bean | No, add a small filter |
| Tomcat 9 (any WAR) | A filter declared in web.xml, or the proxy in front | No, the built-in security filter does not set CSP |
| Angular | The web server or CDN that serves index.html | Nonce 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.
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.
- Component styles are injected as inline
<style>elements. Astyle-src 'self'policy blocks them unless you add'unsafe-inline'or give Angular a nonce. - Bundles are normal script files. With the default ahead-of-time build,
script-src 'self'works and you do not need'unsafe-eval'.
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.
- They add a report-only policy to the API's
SecurityFilterChainwithdefault-src 'none'; frame-ancestors 'none'. An API that returns only JSON loads nothing, so the strictest policy is safe. - For the storefront they add the nginx block above as
Content-Security-Policy-Report-Onlyand thengCspNonceplaceholder inindex.html. - 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 byscript-src, and product photos fromimages.example.comare refused byimg-src. - They add
connect-src 'self' https://api.example.com, the payment domain toscript-srcandframe-src, andhttps://images.example.comtoimg-src. - 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
- Policies in two places. Spring Security plus an nginx
add_headermeans two headers. Both are enforced, so pick one owner for the policy. - Error pages. Responses from Tomcat's default error handling or a proxy's 502 page may not pass through your filter. Test a missing URL with curl as well.
- nginx inheritance. An
add_headerinside alocationblock replaces all those set at server level. Repeat your headers in that block or use an include file. - Angular critical CSS inlining. The production build can inline critical CSS and load the rest with an
onloadattribute, which a strict policy blocks. If the Console reports it, setinlineCriticalto false under the styles optimisation options. - Third-party scripts loading more scripts. Tag managers and widgets often pull in further domains. Adding
'strict-dynamic'alongside a nonce lets scripts you trusted load their own dependencies without listing every host. - Meta tags. A
<meta http-equiv="Content-Security-Policy">tag works for most directives, but it ignoresframe-ancestors,report-uriandsandbox, and scanners that only read headers will still report the header as missing.
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.
- Keep the policy string in one constant or configuration value, under version control, with a comment for each third-party source saying what it is for.
- Run the curl check in your deployment pipeline and fail the build if the header is missing or appears twice.
- Leave a reporting endpoint in place after you enforce, so you hear about breakages from real visitors, not from support tickets.
- Review the policy when you remove a supplier. Sources nobody uses any more are just extra places an attacker could host a script.
- Re-scan after framework upgrades. Spring Security, Tomcat and Angular each changed how headers or nonces work between major versions.
The security audit guide explains how CSP fits alongside the other headers that scanners check.
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.
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 โ