一覧SD-04-1 · CSRF対策の実装

CSRFの仕組み

要求が送られること、JavaScriptが応答を読めること、業務状態が変わることを分けて説明します。

説明

CSRFでは、利用者のブラウザが持つ認証情報を伴って、利用者の意図しない要求が送られることが問題になります。Cookieがあることはログイン状態の判断材料ですが、その操作を利用者が意図した証拠にはなりません。

残高を10減らすHTTP APIへ、別オリジンを示す要求を送ります。Cookieの有無、GETとPOST、CORSのプリフライトを比較し、応答と残高を対応付けます。

要求が送られること、JavaScriptが応答を読めること、業務状態が変わることを分けて説明します。

1開始時の残高を読む

aliceとbobの残高は100ずつです。このAPIには最初、操作の意図を確認するCSRF検証がありません。

from sd04 import CSRF lab = CSRF()独立した残高APIを用意する print(lab.state())

認証CookieとCSRF検証の役割を分けて比較します。以後のHTTPクライアントは指定した固定ヘッダーだけを送ります。

2別オリジンからCookieなしで要求する

Originを他オリジン、認証Cookieをなしにします。

print(lab.request(session=None認証Cookieを付けずに要求する)) print(lab.state())

401で残高は変わりません。このAPIはセッションを持たない相手の操作を受け入れていません。

3同じ要求へ認証Cookieを付ける

Originは同じ他オリジンのまま、aliceのCookieを付けます。

print(lab.request()他オリジンを示すCookie付きPOSTを送る) print(lab.state())

200でaliceが90になります。ログインしているという条件だけで更新すると、要求元を確認していない処理が通ります。

4Cookieと要求元の記録を確認する

受信したメソッド、Cookieの有無、Origin、Fetch Metadataを読みます。

print(lab.last受信側が見た要求の記録を読む)

CookieありでもOriginはother.example.testです。Sec-Fetch-Siteもcross-siteですが、この未対策APIは検査していません。手動HTTPクライアントでのヘッダー設定と、ブラウザが付ける条件は別に確認します。

5GETを状態変更へ使わない

同じ操作先へGETを送ります。

print(lab.request(method="GET"状態変更先へGETを送る)) print(lab.state())

405で残高90を保ちます。GETを拒否することは必要な設計の一つですが、POSTもフォームなどから送れるため、メソッドだけでCSRF対策が完了するわけではありません。

6プリフライト自体では更新しない

OPTIONSを送り、業務操作が実行されないことを確認します。

print(lab.request(method="OPTIONS"プリフライトのメソッドを送る)) print(lab.state())

204で残高は同じです。このAPIはCORS許可ヘッダーを返しません。ただし単純なフォーム形式のPOSTはプリフライトを伴わず送られる場合があります。

7フォーム形式のPOSTを再度送る

フォームのapplication/x-www-form-urlencoded形式で、同じCookie付きPOSTをもう一度送ります。

print(lab.request(body="amount=10"フォーム形式の本文で状態変更を要求する)) print(lab.state()) lab.close()

aliceは80です。ブラウザで応答の読取りがCORSにより拒否されても、送信済み要求の更新が取り消されるわけではありません。Cookieの送信可否にはSameSiteなどの条件も関わります。

まとめ

認証Cookieだけでは、利用者の操作意図を確認できません。

メソッド、Cookie、要求元、保存結果を対応付けます。

CORSの応答読取り制限をCSRF防止の代わりにしません。

操作履歴と解説を振り返れます。

Python 3.15 / HTTP API・SQLite / 固定のCookieと要求

1
from sd04 import CSRF lab = CSRF() print(lab.state())
[('alice', 100), ('bob', 100)]
2
print(lab.request(session=None)) print(lab.state())
(401, {'error': 'session'})
[('alice', 100), ('bob', 100)]
3
print(lab.request()) print(lab.state())
(200, {'subject': 'alice', 'amount': 90})
[('alice', 90), ('bob', 100)]
4
print(lab.last)
{'method': 'POST', 'cookie_present': True, 'origin': 'http://other.example.test', 'site': 'cross-site'}
5
print(lab.request(method="GET")) print(lab.state())
(405, {'error': 'method'})
[('alice', 90), ('bob', 100)]
6
print(lab.request(method="OPTIONS")) print(lab.state())
(204, None)
[('alice', 90), ('bob', 100)]
7
print(lab.request(body="amount=10")) print(lab.state()) lab.close()
(200, {'subject': 'alice', 'amount': 80})
[('alice', 80), ('bob', 100)]

演習完了