Content-Security-Policy Nonce with Spring Security
Content-Security-Policy is important for web security. Yet, it’s not mainstream yet, it’s syntax is hard, it’s rather prohibitive and tools rarely have flexible support for it.
While Spring Security does have a built-in Content Security Policy (CSP) configuration, it allows you to specify the policy a a string, not build it dynamically. And in some cases you need more than that.
In particular, CSP discourages the user of inline javascript, because it introduces vulnerabilities. If you really need it, you can use unsafe-inline
but that’s a bad approach, as it negates the whole point of CSP. The alternative presented on that page is to use hash
or nonce
.
I’ll explain how to use nonce with spring security, if you are using .and().headers().contentSecurityPolicy(policy)
. The policy string is static, so you can’t generate a random nonce for each request. And having a static nonce is useless. So first, you define a CSP nonce filter:
public class CSPNonceFilter extends GenericFilterBean { private static final int NONCE_SIZE = 32; //recommended is at least 128 bits/16 bytes private static final String CSP_NONCE_ATTRIBUTE = "cspNonce"; private SecureRandom secureRandom = new SecureRandom(); @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; byte[] nonceArray = new byte[NONCE_SIZE]; secureRandom.nextBytes(nonceArray); String nonce = Base64.getEncoder().encodeToString(nonceArray); request.setAttribute(CSP_NONCE_ATTRIBUTE, nonce); chain.doFilter(request, new CSPNonceResponseWrapper(response, nonce)); } /** * Wrapper to fill the nonce value */ public static class CSPNonceResponseWrapper extends HttpServletResponseWrapper { private String nonce; public CSPNonceResponseWrapper(HttpServletResponse response, String nonce) { super(response); this.nonce = nonce; } @Override public void setHeader(String name, String value) { if (name.equals("Content-Security-Policy") && StringUtils.isNotBlank(value)) { super.setHeader(name, value.replace("{nonce}", nonce)); } else { super.setHeader(name, value); } } @Override public void addHeader(String name, String value) { if (name.equals("Content-Security-Policy") && StringUtils.isNotBlank(value)) { super.addHeader(name, value.replace("{nonce}", nonce)); } else { super.addHeader(name, value); } } } }
And then you configure it with spring security using: .addFilterBefore(new CSPNonceFilter(), HeaderWriterFilter.class)
.
The policy string should containt `nonce-{nonce}`
which would get replaced with a random nonce on each request.
The filter is set before the HeaderWriterFilter
so that it can wrap the response and intercept all calls to setting headers. Why it can’t be done by just overriding the headers after they are set by the HeaderWriterFiilter, using response.setHeader(..) – because the response is already committed and overriding does nothing.
Then in your pages where you for some reason need inline scripts, you can use:
(I’m using the Pebble template syntax; but you can use any template to output the request attribute “csp-nonce”)
Once again, inline javascript is rarely a good idea, but sometimes it’s necessary, at least temporarily – if you are adding a CSP to a legacy application, for example, and can’t rewrite everything).
We should have CSP everywhere, but building the policy should be aided by the frameworks we use, otherwise it’s rather tedious to write a proper policy that doesn’t break your application and is secure at the same time.
Published on Java Code Geeks with permission by Bozhidar Bozhanov, partner at our JCG program. See the original article here: Content-Security-Policy Nonce with Spring Security Opinions expressed by Java Code Geeks contributors are their own. |
I have implemented this in my application, however, it only applies nonce to authenticated pages (after authentication). How would I implement it in an un-authenticated page, such as maybe the login page?