「ログイン中に、別のサイトを開いただけで、意図しない操作が実行される」——CSRF(クロスサイトリクエストフォージェリ)は、こうした攻撃です。
GETとPOSTの違いとセキュリティ でも軽く触れましたが、この記事ではCSRFの仕組みと、トークンによる対策方法を詳しく整理します。
セキュリティ対策を横断的に確認したい方は、 Webアプリの脆弱性・セキュリティ対策まとめ もご覧ください。
CSRFが成立する前提は、「ブラウザはCookieを、リクエストの送信元がどこであっても自動的に付けて送る」という仕組みにあります。
例えば、あなたがオンラインバンキングにログインした状態のまま、別のタブで悪意のあるサイトを開いたとします。そのサイトに、次のような隠しフォームが仕込まれていたらどうなるでしょうか。
<form action="https://bank.example.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker">
<input type="hidden" name="amount" value="10000">
</form>
<script>document.forms[0].submit()</script>
このフォームがページ読み込み時に自動送信されると、ブラウザはbank.example.com宛てのリクエストとしてログインCookieを付けて送信します。銀行のサーバーから見ると「ログイン中の本人からの正規のリクエスト」にしか見えません。あなたは何もクリックしていないのに、送金が実行されてしまいます。
実際に手を動かして確かめてみましょう。以下のデモは実際の通信を一切発生させません。「正規サイト」と「わなサイト」を模した2つのカードで、CSRFトークンの有無による違いをその場で確認できます。
実際の通信は発生しません。あなたが「正規サイトにログイン中」という想定で、右側の「わなサイト」を開いたときに何が起きるかを確認してください。
あなた自身が意図して押す、正規の操作です。
実際には「面白い画像はこちら」のようなリンクの裏で、送金フォームが自動送信される想定です。
上のボタンを押すと、実行結果がここに表示されます。
「CSRFトークンなし」の状態で「このページを開く(体験用)」を押すと、あなたは何も操作していないのに送金が成立してしまったはずです。「CSRFトークンあり」に切り替えると、同じボタンを押してもサーバー側でリクエストが拒否され、残高は変化しません。これがCSRFトークンの効果です。
CSRF対策として広く使われているのが、CSRFトークンという仕組みです。
外部の悪意あるサイトは、正規サイトが発行したこのトークンの値を知ることができません。そのため、隠しフォームを自動送信しても、トークンが一致せずリクエストが拒否されます。
Djangoでは{% csrf_token %}タグをフォーム内に置くだけで、この仕組みが自動的に組み込まれます。多くのフレームワークで、同様の仕組みが標準機能として用意されています。
もう1つの有効な対策が、CookieのSameSite属性です。
SameSite=LaxやSameSite=Strictを設定すると、別サイトからのリクエストにはそもそもCookieが自動的に付与されなくなります。これにより、CSRFトークンとは別の層でも攻撃を防げます。
CSRFトークンとSameSite属性は排他的なものではなく、両方を設定しておくのが実務上の基本です。
SameSite=Lax以上を設定する
CSRFは、ログイン中のCookieが自動的に送信される仕組みを悪用した攻撃です。対策の基本はCSRFトークンによる検証と、CookieのSameSite属性の設定の組み合わせです。
自分のアプリの状態変更フォームに、この2つがきちんと実装されているか、この機会に確認してみてください。