調べものをしている中で、今さらながらSame-Site Cookieの仕様書を斜め読みした。
ググって最初に見つけた仕様の中で、same-siteは下のように定義されていた。
A request is "same-site" if its target's URI's origin's registrable domain is an exact match for the request's initiator's "site for cookies", and "cross-site" otherwise.
Same-site Cookies draft-west-first-party-cookies-07 2.1 (April 6, 2016)
対象(target)と始点(initiator)のサイトが一致すればsame-siteということになる。
リダイレクトを挟むとどうなるか
上の仕様を読んで疑問に思ったのは、始点と対象の間にリダイレクトが挟まっていたらどうなるかということ。
① A上にあるBへのリンクをクリック
Aサイトのページ <a href="https://site-B/">Bサイトへ</a>
② Bは302を返しAにリダイレクト
HTTP/1.1 302 Found Location: https://site-A/donateMoney?to=Bob&amount=10000
③ リダイレクトによりAに返ってくる
https://site-A/donateMoney?to=Bob&amount=10000
上で引用した定義に従うと、③のリクエストは始点である①のページとサイトが同じなのでsame-siteになり、StrictやLaxのCookieが送られてしまう。Googleの検索結果のページを例にすれば、検索結果から別のサイトに遷移すると、リダイレクトを使ってGoogleのサイトの任意のURLに対してGETのsame-siteなリクエストを打ち込めることになる。
POSTであっても同じで、仮にAサイト上にPOSTでBにsubmitするフォームがあるとすると、①でAからBにsubmitし、②でBがステータス307を返すと、③でsame-siteのPOSTのリクエストがAに送られてしまう。POSTのボディについてはいじれないが、クエリストリングとボディのパラメータを区別しないことはままあるので、そうであればsame-siteによるCSRF対策は破れることになる。
実際のブラウザで調べると、Chromeはそのとおりの挙動だった。下は各ブラウザにおいて、最後のAへのリクエストで送られるCookieを表にしたもの。
ブラウザ | GET A -> B -> A | POST A -> B -> A | POST A -> A -> B -> A | バージョン |
Chrome | Strict/Lax/None | Strict/Lax/None | Strict/Lax/None | 92.0.4515.131 |
IE | Strict/Lax/None | None | Strict/Lax/None | Windows10 21H1 (OS Build 19043.1165) |
Firefox | Lax/None | None | None | 91.0.1 |
Safari | Lax/None | None | None | 14.1.2 (15611.3.10.1.5) |
このようにsame-siteの定義はブラウザによってブレがある。Chromeは上の仕様通り実装されており、ある意味で具合が良くない。Firefox, Safariは始点との間にリダイレクトがあるとsame-siteとはみなさない。IEはまた少し違う。
CSRF対策をsame-siteのみに頼るのはそもそもどうなのかという話もあり微妙ではあるが、仕様かChromeのバグとして報告しようかと思って、もう少し調べてみた。分かりやすい問題なので、既に誰かが報告しているんじゃなかと思ったら、やはりその通りだった。
Issue 1221316: SameSite context type should consider redirect chain
仕様自体も、上記の私が引用していたのは実は古いもので、最新のものには以下のPRが反映されていて、リダイレクトが考慮されていることが分かった。
という訳で、しばらくしたらChromeもFirefoxやSafariと同様に、始点と終点だけではなく間のリダイレクトも考慮した上で、same-site判定することになるのだろう。
Fetchはどうか
以上はNavigationの話であったが、次はJSでのfetch()
におけるリダイレクトについて見てみたい。
① AのページからBをfetch
② Bは302を返しAにリダイレクト
③ リダイレクトによりAに返ってくる
という流れ。
下手をすると、AのページはBのリソースをfetchしているつもりだが、実はauthenticatedなAのリソースをつかんでしまうことになり、具合が悪い。
どうなるのかの結論を言うと、リダイレクトでオリジンが変わる時に内部的にtainted origin flagが立てられる。すると、③のリクエストのOriginはnullになり、AからAをfetchしているにも関わらず③の応答にAccess-Control-Allow-Origin: null
が無いと応答が取れないはめになる。初期の頃のfetch仕様からこのような動作が規定されていたようで、全てのブラウザで同じ挙動だった。
送られるCookieについては、先のNavigationのリダイレクトと大体同じになる。つまり現状のChromeでは、③はsame-siteとみなされて、Strictを含めたCookieが送られる。一方でFirefoxでは③にはNoneのCookieのみが送られる(Top level navigationではないのでLaxのCookieは送られない)。Safariでは、3rd party cookieの扱いのせいだと思われるが、③にはCookieが送られない。
なかなか奥が深い...