Spring SecurityでCSP(Content-Security-Policy)対策してみた話

こんにちは、猫好きリーマンのほげPGです。
ある日、いつものようにOWASP ZAPでセキュリティチェックをしていたら、こんな警告が出ました。
「Content-Security-Policyヘッダーが設定されていません!」
……というわけで、今回は CSP(Content-Security-Policy)対策 をSpring Securityでどう実装するか、実際にやってみた内容をまとめてみます。
そもそもCSPって?
CSPは、ブラウザが読み込むリソースの種類や出所を制限することで、XSS(クロスサイトスクリプティング)などの攻撃を防ぐための仕組みです。
中でも特に厳しいのが script-src と style-src の制限。これらを適切に設定しないと、ZAP先生に怒られます。
怒られたコードたち
以下のようなコードがCSP的にはNGとされます。
怒られるコード①:onclick に直接JavaScriptを書く
<form name="logoutForm" action="/logout" method="POST"> <a onclick="logoutForm.submit();">Logout</a></form>
怒られるコード②:<script> タグ内に直接JavaScriptを書く
<script>$(window).on("load", function() { …});</script>
怒られるコード③:style 属性に直接CSSを書く
<div style="background: green;"><h3>トップ</h3></div>
怒られるコード④:<style> タグ内に直接CSSを書く
<style> .cusor-pointer { cursor : pointer; }</style>
つまり、インラインのJavaScriptやCSSは全部アウトということですね。
対策方法:インラインコードを外に出す
基本的な対策はシンプルで、
- JavaScript → .js ファイルに分離
- CSS → .css ファイルに分離
という形にします。
ただし、既存プロジェクトでこれをやるのは正直しんどい……。ファイル分け、IDの整理、共通化など、考えることが山積みです。
現実的な対応:nonce を使う
そこで登場するのが nonce(ナンス)という仕組み。
CSPヘッダーに以下のように設定します:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-xxxx'; style-src 'self' 'nonce-xxxx';
※ xxxx はランダムな文字列(毎回変わる)
HTML側では、<script> や <style> タグに nonce 属性を付けることで、インラインコードでも許可されるようになります。
<script nonce="xxxx">…</script>
Spring Bootでの実装例
Spring SecurityでCSPヘッダーを設定するには、以下のようにします。
WebSecurityConfig.java
@Configuration@EnableWebSecuritypublic class WebSecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http … .headers(headers -> headers .contentSecurityPolicy(csp -> csp .policyDirectives("default-src 'self' 'nonce-{nonce}';" + " frame-ancestors 'none'; form-action 'self';") )) .addFilterBefore(new CspNonceFilter(), HeaderWriterFilter.class) .addFilterBefore(new LoginUserFilter(), UsernamePasswordAuthenticationFilter.class) … return http.build(); } public static class CspNonceFilter implements Filter { private static final SecureRandom secureRandom = new SecureRandom(); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; String uri = httpRequest.getServletPath(); if (!checkStaticPath(uri)) { byte[] nonceBytes = new byte[16]; secureRandom.nextBytes(nonceBytes); String nonce = Base64Utils.encodeToString(nonceBytes); request.setAttribute("cspNonce", nonce); chain.doFilter(request, new CspNonceResponseWrapper((HttpServletResponse) response, nonce)); } else { chain.doFilter(request, response); } } private boolean checkStaticPath(String path) { return path.startsWith("/static/"); } } public static class CspNonceResponseWrapper extends HttpServletResponseWrapper { private final String nonce; public CspNonceResponseWrapper(HttpServletResponse response, String nonce) { super(response); this.nonce = nonce; } private String getHeaderValue(String name, String value) { if (name.equals("Content-Security-Policy") && StringUtils.hasText(value)) { return value.replace("{nonce}", nonce); } else { return value; } } @Override public void setHeader(String name, String value) { super.setHeader(name, getHeaderValue(name, value)); } @Override public void addHeader(String name, String value) { super.addHeader(name, getHeaderValue(name, value)); } } @ControllerAdvice public static class CspNonceControllerAdvice { @ModelAttribute public void addAttributes(Model model, HttpServletRequest request) { model.addAttribute("nonce", request.getAttribute("cspNonce")); } }}
テンプレート側の対応(Thymeleaf)
Thymeleafを使っている場合は、テンプレートに th:nonce=”${nonce}” を書くだけでOK。
… <form name="logoutForm" th:action="@{/logout}" method="post" class="ms-auto link-light"> <a id="logout" th:text="#{auth.logout}">Logout</a> </form>… <script th:nonce="${nonce}"> $(window).on("load", function() { $("#logout").on('click', function(e) { document.logoutForm.submit(); }); }); </script>…
イベントハンドラやクラスの切り替えなど、多少の工夫は必要ですが、構成を大きく変えずに済むのが嬉しいポイントです。
まとめ
- CSPはセキュリティ対策として重要
- インラインコードは基本NG
- nonce を使えば、既存構成を大きく変えずに対応可能
- Spring Security + Thymeleafなら比較的スムーズに導入できる
プロジェクト一式も用意してあるので、興味ある方はぜひ参考にしてみてください!
※元文書を適当に作成しCopilotで生成