CSRF対策の基本的な仕組みとは?初心者向けにわかりやすく解説
新人
「Webセキュリティの勉強をしていたら『CSRF』って言葉が出てきたんですけど、それって何なんですか?」
先輩
「CSRFは『クロスサイトリクエストフォージェリ』と呼ばれる攻撃手法の一つで、ユーザーの意図しない操作をWebアプリケーションにさせる危険な仕組みだよ。」
新人
「ユーザーが知らないうちに操作されるってことですか?どんな感じなんでしょう?」
先輩
「それじゃあ、CSRF攻撃の基本と仕組みを順番に解説していこう!」
1. CSRF(クロスサイトリクエストフォージェリ)とは?
CSRFとは、「Cross-Site Request Forgery(クロスサイトリクエストフォージェリ)」の略で、日本語では「サイト間リクエスト偽造」と呼ばれます。これは、ログイン中のユーザーが意図しない操作をさせられる攻撃です。
例えば、ユーザーがログインしたままの状態で、悪意あるサイトにアクセスしたときに、攻撃者が用意したリクエストが自動的に送信され、ユーザーが意図しない更新や削除操作が実行されることがあります。
CSRFは、ユーザーの「信頼されたセッション情報(クッキーなど)」を利用して、正規のリクエストを偽装するのが特徴です。だからこそ、Webアプリケーション側でCSRF対策が重要になります。
2. なぜWebアプリケーションにとって危険なのか
CSRF攻撃が危険な理由は、攻撃者がユーザーの認証情報を悪用して「正規の操作」としてリクエストを送信できてしまう点にあります。
たとえば、ユーザーがインターネットバンキングにログインした状態で、別の悪質なサイトを閲覧するとします。すると、その悪質なサイトに仕込まれたスクリプトが、自動的に振込処理を行うリクエストを銀行に送るということが可能になります。
このとき、Webアプリ側が「このリクエストは本当にユーザーが意図したものか」を確認できないと、重大な被害につながります。だからこそ、CSRF対策はWebセキュリティにおいて非常に重要な位置づけになります。
3. 実際にどういった攻撃シナリオが考えられるのか(簡単な例)
ここでは、CSRF攻撃がどのように行われるかの簡単な例を紹介します。以下は、攻撃者が自分のWebサイトに以下のようなHTMLを埋め込んだケースです。
<form action="http://example.com/user/update-email" method="POST">
<input type="hidden" name="email" value="hacker@example.com" />
<input type="submit" value="送信" />
</form>
<script>
document.forms[0].submit();
</script>
このHTMLは、あるユーザーが攻撃者のWebサイトを開いたときに自動的にフォームが送信されて、メールアドレスが「hacker@example.com」に変更されてしまうという仕組みです。
もしこのとき、ユーザーがexample.comにログインしていて、セッション情報(クッキー)がブラウザに残っていれば、攻撃が成立してしまいます。
このような攻撃は、フォーム以外にも、画像タグやリンクなど、さまざまな手段で実行可能です。そのため、すべてのPOSTリクエストに対してCSRF対策が必要になります。
4. CSRF対策がSpring Securityに標準で組み込まれていることの紹介
Spring Securityでは、CSRF対策が最初から有効になっているため、特別な設定をしなくても、基本的な保護が働きます。これは、Spring Securityがセキュリティに強い理由の一つであり、開発者がミスをしにくい設計になっています。
たとえば、Spring BootプロジェクトにSpring Securityを追加した場合、POST・PUT・DELETEといった状態を変更するようなリクエストに対して、CSRFトークンが自動的にチェックされるようになります。
この仕組みにより、攻撃者が偽のフォームを送信しても、正しいトークンが含まれていなければリクエストは拒否されます。つまり、Spring Securityは初期状態でCSRFトークンによる保護を行うのです。
逆に言えば、CSRFトークンがフォームに含まれていない場合、Spring Securityによって403エラーが返されることになります。
5. Spring SecurityのCSRF対策の有効化とデフォルトの挙動
Spring Securityでは、CSRF対策がデフォルトで有効になっていますが、設定の確認やカスタマイズも可能です。Spring Bootを使った場合、以下のようにセキュリティ設定クラスを作成して確認できます。
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/").permitAll()
.anyRequest().authenticated()
)
.csrf(Customizer.withDefaults()); // CSRF対策をデフォルト有効化
return http.build();
}
}
上記のように、csrf(Customizer.withDefaults())を記述することで、Spring Securityが持つCSRF保護機能をそのまま有効にできます。この状態では、POSTやPUTなどのリクエストで、CSRFトークンが一致しないと403エラーになります。
フォーム画面では、トークンをHTMLに埋め込む必要があるため、後述の方法でトークンを自動挿入する工夫が求められます。
6. CSRFトークンの仕組み(生成・検証・送信の流れ)
CSRF対策の核となるのが「CSRFトークン」です。これは、Webアプリケーションがユーザーごとに一意の値を発行し、それをフォームに埋め込むことで、正規のリクエストかどうかを判断する仕組みです。
Spring Securityでは、CSRFトークンはリクエストごとに自動生成され、セッションに紐付けられて保存されます。そして、フォームが描画されるタイミングで、そのトークンをHTMLの中に埋め込むことが求められます。
例えば、次のようなHTMLフォームでCSRFトークンを送信する必要があります。
<form method="post" action="/account/update">
<input type="hidden" name="_csrf" value="${_csrf.token}" />
<input type="text" name="username" />
<button type="submit">更新</button>
</form>
上記のように_csrfという名前のhiddenフィールドに、CSRFトークンをセットして送信します。Spring Securityは、リクエストを受け取った際に、このトークンがセッションに保存されているものと一致するかを検証します。
一致していればリクエストを許可し、不一致や未指定の場合は403 Forbiddenエラーとなります。これにより、不正な外部サイトからのリクエストが弾かれる仕組みになっているのです。
CSRFトークンは、フォームからだけでなく、JavaScriptを使ったAjax通信にも必要です。その場合、HTTPヘッダーにトークンを埋め込むなどの工夫が必要になります。
たとえば、Ajaxで送信する際は、次のようにトークンをヘッダーに含めることが一般的です。
<script>
const csrfToken = document.querySelector('meta[name="_csrf"]').getAttribute('content');
fetch('/api/update', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': csrfToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({ key: 'value' })
});
</script>
このように、CSRF対策では「どのようにトークンを埋め込み、送信し、検証するか」をきちんと理解しておくことが非常に重要です。
CSRFトークンは、セッションと結びついていて、第三者が推測できない値であるため、セキュリティレベルを高める有効な手段となります。
Spring Securityは、これらのトークン処理を自動で行ってくれる仕組みが備わっているため、適切に使えば高い安全性を確保することが可能です。
7. カスタマイズしたCSRF設定(特定のエンドポイントを除外する方法など)
Spring SecurityのCSRF対策は強力ですが、すべてのエンドポイントに対して一律に適用されるわけではありません。開発中や一部のAPI連携では、特定のパスだけCSRF保護を除外したいケースもあります。
たとえば、外部サービスからのPOSTリクエストを受けたい場合や、Webhook連携などでは、トークンの確認が難しいことがあります。このような場合は、セキュリティ設定で「CSRFトークン除外」の対象を個別に指定できます。
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/").permitAll()
.anyRequest().authenticated()
)
.csrf(csrf -> csrf
.ignoringRequestMatchers("/webhook/**") // CSRFトークンを除外
);
return http.build();
}
}
ignoringRequestMatchers()を使えば、CSRF制御の対象外としたいURLパターンを指定できます。上記の例では、/webhook/**パス以下のリクエストはCSRFトークンなしでも許可されます。
ただし、このような除外設定を安易に増やすとセキュリティリスクが高まります。特に認証が必要な管理系のAPIを除外するのは、アンチパターンです。CSRFトークン除外は、必ず「必要最小限の安全な範囲でのみ」行うようにしましょう。
8. 実際のフォームでCSRFトークンを扱う方法(Thymeleafなどを使わず基本的なformで解説)
実際のWeb画面で、CSRFトークンを正しく送信するためには、HTMLのフォームに手動でトークンを埋め込む必要があります。Thymeleafなどのテンプレートエンジンを使っていない場合は、コントローラ側でトークン情報をビューに渡すことで対応します。
Spring Securityでは、CsrfTokenオブジェクトを取得して、モデルに追加することで、フォーム内で使用できます。
@Controller
public class FormController {
@GetMapping("/form")
public String showForm(Model model, CsrfToken token) {
model.addAttribute("_csrf", token);
return "form";
}
}
このようにCsrfTokenを受け取ることで、HTML内にトークンを埋め込むことができます。ビュー(form.htmlなど)では、以下のようにhiddenフィールドを記述します。
<form method="post" action="/submit">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
<input type="text" name="username" />
<button type="submit">送信</button>
</form>
ポイントは、_csrf.parameterNameと_csrf.tokenを適切に出力することです。これにより、Spring Securityが期待するパラメータ名とトークン値が正しく送信され、CSRF検証が成功します。
初心者のうちはトークンの取得方法や名前の扱いでつまずきやすいですが、必ずhiddenフィールドとしてトークンを明示的に送信することを意識しましょう。
9. CSRFを無効化した場合のリスクや注意点
開発中やデバッグ中に、403 Forbiddenエラーが出て動かなくなると、「CSRF無効化すればいいのでは?」という考えが浮かびがちです。しかし、これは非常に危険な選択です。
CSRF対策を無効にすると、攻撃者がユーザーのセッションを利用して、不正な操作を実行することが可能になります。たとえば、以下のような設定はセキュリティ的に問題があります。
// ※やってはいけない例(アンチパターン)
http.csrf(csrf -> csrf.disable());
この設定により、全てのリクエストに対してCSRFチェックが無効化されます。つまり、攻撃者が偽装リクエストを送ってもブロックされなくなってしまいます。特にログイン後の機能(会員情報更新や購入処理など)があるシステムでは、被害が深刻化する可能性があります。
どうしても一時的に無効化が必要な場合でも、開発用の環境だけに限定し、本番環境では必ず有効に戻すようにしてください。
Spring Securityは、CSRFトークンを自動で管理してくれるため、基本的にはその仕組みに乗っかるだけで安全性が高まります。CSRFトークン除外やCSRF無効化のリスクを正しく理解し、安易にdisable()を使わないことがWebアプリの信頼性向上につながります。