現場で数々のWebアプリケーションの惨状を見てきた身として、今日は「CSRF(クロスサイトリクエストフォージェリ)対策の決定版」である SameSite 属性について、建前抜きで語ろうと思う。
「CSRF対策? CSRFトークンを入れればいいんでしょ?」なんて思っているなら、それはもう古い。確かにCSRFトークンは強力だが、実装漏れや設計ミスが必ず発生する。SameSite 属性は、ブラウザレベルで強制力を持つ強力な防波堤だ。ここを理解せずして現代のセキュアなWeb開発は語れない。
1. なぜ「今さら」SameSiteなのか
CSRFは、攻撃者が「ターゲットがログインしている状態で、悪意あるサイトからターゲットのブラウザを操り、意図しないリクエストを送信させる」攻撃だ。これに対し、SameSite 属性は「Cookieをクロスサイトのリクエストで送信させるか?」をブラウザに指示する。
設定値は主に3つだ。
Strict: 最も堅牢。完全に同一サイト内でのみCookieが送信される。別サイトからのリンク遷移ですらCookieが送られないため、利便性は犠牲になる。Lax: デフォルト(現代のブラウザ)。トップレベルのナビゲーション(リンククリックなど)では送信されるが、<img>や<iframe>、fetchやXMLHttpRequestなどの「副作用を伴うリクエスト」では送られない。None: どこからでも送る。これを使う場合はSecure属性が必須。サードパーティクッキーとしてトラッキング等に使われる。
現場で見かける最悪のケースは、SameSite=None を不用意に設定し、かつ Secure を忘れているケースだ。これは「どうぞ私のサイトを攻撃してください」と言っているに等しい。
2. 現場で刺さる「攻撃のシナリオ」
攻撃者はどう動くか。例えば、ユーザーのメールアドレスを変更するAPIが POST メソッドで実装されており、CSRF対策が皆無だとしよう。
攻撃者は以下のようなHTMLを仕込んだ罠サイトを用意する。
<!-- ターゲットのブラウザで、気づかぬうちにリクエストを送信させる -->
<form id="csrf-form" action="https://bank.example.com/api/update-email" method="POST">
<input type="hidden" name="email" value="attacker@evil.com">
</form>
<script>
// 自動的にフォームを送信する
document.getElementById('csrf-form').submit();
</script>
もし、あなたのサイトのCookieに SameSite 属性が設定されていない、あるいは None になっていると、この POST リクエストは認証済みCookieを伴って送信されてしまう。結果、被害者のメールアドレスは書き換えられ、アカウントは奪取される。
3. 実務で即採用すべきセキュアな実装
では、どう守るか。最も確実なのは、Cookieの設定時に SameSite=Lax をデフォルトにすることだ。
PHPでのセッション設定(php.ini)
PHP 7.3以降であれば、セッション設定で簡単に制御できる。php.ini を直接書き換えるか、コード内で指定する。
// php.iniの設定例
session.cookie_samesite = "Lax"
session.cookie_secure = true
// コード内で指定する場合(PHP 7.3+)
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => 'example.com',
'secure' => true, // HTTPS必須
'httponly' => true, // JavaScriptからのアクセスを禁止
'samesite' => 'Lax' // CSRF防御の肝
]);
session_start();
Python (Django) の場合
Djangoは非常に優秀で、設定ファイル(settings.py)で一括管理できる。
# settings.py
SESSION_COOKIE_SAMESITE = 'Lax'
SESSION_COOKIE_SECURE = True # HTTPS環境が前提
CSRF_COOKIE_SAMESITE = 'Lax'
Nginxで強制的に付与する
もしアプリケーションコードに手を入れるのが難しいレガシーシステムであれば、リバースプロキシ(Nginx)でヘッダーを強制的に上書きするのも一つの手だ。
# nginx.conf
# すべてのSet-CookieヘッダーにSameSite=Laxを追加する
proxy_cookie_path / "/; SameSite=Lax; Secure";
4. 注意すべき「落とし穴」
「じゃあ全部 Strict にすれば最強じゃん」と思うかもしれないが、そうではない。
Strict にすると、外部サイトのリンクからあなたのサイトへ遷移した直後、ユーザーは「ログアウト状態」に見えてしまう。利便性を損なうため、UXが悪化する。
また、SameSite=Lax であっても、GETメソッドによるリクエストは防げない。もし君のアプリが GET リクエストでデータの変更(例: GET /delete-user?id=123)を行っているなら、それは SameSite 云々の前にアーキテクチャの欠陥だ。Webの原則である「GETは読み取り専用」を徹底してほしい。
最後に:エンジニアへのメッセージ
セキュリティは「魔法の杖」を振って終わりではない。SameSite は強力な防御策だが、あくまで多層防御の一環だ。
1. GETで状態を変更しない(RESTfulの基本)
2. 重要な処理にはCSRFトークンを併用する(二重の守り)
3. SameSite=Lax をベースに、要件に応じて使い分ける
この3つを守るだけで、君のプロダクトは大多数の「攻撃されやすいサイト」から脱却できる。コードを書くとき、サーバーを立てるとき、「これは攻撃者にどう悪用されるか?」と常に自問自答してほしい。その一瞬の疑念が、明日の事故を防ぐんだ。
コメント