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
@EnableWebSecurity
public 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で生成