こんにちは、SSTでWeb脆弱性診断用のツール(スキャンツール)開発をしている坂本(Twitter, GitHub)です。
最近 Fetch API*1 や XMLHttpRequest*2 を使ってクロスサイトのCookieを送る・送らないの実験をする機会がありました。 そこで自分の勘違いが複数発見され、改めて「今のクロスサイト Cookie って難しいな」と感じました。 セキュリティエンジニアのみならず、開発者の方にも参考になるかと思いましたので、自分の勘違いを紹介していきたいと思います。
なお本記事では「サイト」という単語を Cookie における Same-Site の定義に沿って扱います。
Understanding "same-site" and "same-origin"
使用したWebブラウザ:
- Chrome(burp 同梱のChromium) 106.0.5249.62 (64bit)
- Firefox 106.0.1 (64bit)
- 1. XMLHttpRequest.withCredentials の勘違い
- 2. Schemeful Same-Site の学び
- 3. Firefox 106.0.1 時点では SameSite 属性が無い場合 None 扱いになる
- 4. Firefox と包括的 Cookie 保護機能(Total Cookie Protection)
- まとめと感想
1. XMLHttpRequest.withCredentials の勘違い
SameSite 属性省略時のCookieで実験をしていたときの話です。
同一Origin内で XMLHttpRequest の withCredentials
*3 が true/false, Fetch API の credentials
パラメータ*4 が omit/include
それぞれで、リクエストにCookieが含まれるかどうか、というのを実験していました。(つまりは、同一サイト内でのごく一般的な ajax コールです)
実験結果:
- XMLHttpRequest の
withCredentials
- false(デフォルト) でも true でも Cookie が送られました。
- Fetch API の
credentials
パラメータomit
ではCookieが送られない。include
ではCookieが送られる。
「あれ? withCredentials
って同一サイト間では無視されるのかな?」と混乱してしまったのですが、そういう仕様でした:
XMLHttpRequest.withCredentials - Web API | MDN
これは同じサイトへのリクエストには影響しません。
- 自分の勘違い(誤) : 同一サイトであっても
withCredentials
が Cookie を送る/送らないを切り替える。 - 正解 :
withCredentials
は同一サイトの場合は無視されて、trueでもfalseでも Cookie を送る仕様。
Fetch API の credentials
であれば、同一サイトかどうかに関わらず omit/include
それぞれ期待どおりに動きますので、直感的で分かりやすいと思いました。
2. Schemeful Same-Site の学び
引き続き SameSite 属性省略, さらに Secure属性も省略したCookieで実験をしていたときの話です。 Chrome が http から https への fectch/XHR で Cookie を送らない現象に遭遇しました。
例:
https://foo.example.com/
で SameSite属性無し, Secure 属性無しで Set-Cookieする。http://foo.example.com/
から fetch/XHR((XMLHttpRequest の略) でhttps://foo.example.com/
にリクエストを送る。
このとき、Firefoxであれば以下のように直感的に「その通り」な挙動となりました。
- fetch:
crecentials: omit
→ 1. のcookieは送らない。credentials: include
→ 1. のcookieを送る。
- XMLHttpRequest:
withCredentials: false
→ 1. のcookieは送らない。*5withCredentials: true
→ 1. のcookieを送る。
ところがChromeでは fetch (credentials: include
) も XHR (withCredentials: true
) も、両方とも 1. のcookieを送りませんでした。
弊社CTOの @hasegawayosuke に聞いてみたところ Twitterで代わりに質問してくれて 、 @agektmr さんから回答をもらえました。ありがとうございます!
これですかねhttps://t.co/jfne2ve8Rt
— Eiji Kitamura / えーじ (@agektmr) 2022年8月18日
現在、これは他のサードパーティまたはクロスサイトのサブリソースと同じように処理されるようになっているため、SameSite=Strict または SameSite=Lax Cookie はブロックされます。
Chrome/Chromiumにおいては SameSite属性なしはデフォルトで Lax として扱われるので、これに該当したものと思われます。 実際に devtools の console を見てみると、以下のような Schemeful Same-Site についての error が発生していました。
- 自分の勘違い : fetch (
credentials: include
) も XHR (withCredentials: true
) も、http → https へのリクエストでCookieを送ってくれる。 - 2022年10月時点の現実 : Chrome においては Schemeful Same-Site が導入された。http → https へのサブリソース呼び出しでは Lax 扱いのCookie は送られない。
3. Firefox 106.0.1 時点では SameSite 属性が無い場合 None 扱いになる
さらに引き続き SameSite 属性とSecure属性を省略したCookieで実験をしていたときの話です。 Chrome/Firefoxそれぞれで、devtools から cookie をみていて「あれ?」となりました。
Chrome: SameSite属性欄が空になっていて、これは実際に SameSite属性無しで Set-Cookie しているので想定どおりです。
Firefox: SameSite属性が None と表示されてる・・・!?
Firefox での SameSite 未指定時の扱いについて調べてみると、2020-08-04時点の以下の記事では将来的に Lax 扱いになるとありました。
実際に Firefox 96 の開発者向けリリース記事にはデフォルトが Lax になる記述があります:
In addition, cookies are assumed to implicitly set SameSite=Lax if the SameSite attribute is not specified (previously the default was SameSite=None), and cookies with SameSite=None require a secure context.
ところが2022年1月29日時点では、以下の記事の追記にあるとおり従来の挙動に戻されたようです。
(2022年1月29日追記) 本日確認したところ、Firefoxにおけるデフォルトsamesite=laxはキャンセルされ、従来の挙動に戻ったようです(Firefox 96.0.3にて確認)。 デフォルトsamesite=lax自体は先行してGoogle Chromeにて実装されていましたが、細かい挙動の差異で既存サイトに不具合が生じたようで、いったん巻き戻されたようです。
2022年10月時点の Firefox 106.0.1 で確認すると、about:config → network.cookie.sameSite.laxByDefault
がデフォルトで false になっていました。
- 自分の勘違い : ChromeもFirefoxも、SameSite未指定時はLax扱いになる。
- 2022年10月時点の現実 : Firefox では SameSite未指定時は None 扱いになる。
None扱いだとしたら、セキュリティはどうなるのか?と思いますが、次に紹介する 「包括的 Cookie 保護機能(Total Cookie Protection)」によりある程度担保できているようです。
Firefoxでの Same-Site 未指定時のデフォルトがどうなるか、今後の状況によってまた変わる可能性はありそうです。
4. Firefox と包括的 Cookie 保護機能(Total Cookie Protection)
「3. 」からの続きで、実際にFirefoxの でクロスサイトの Fetch API を試してみると「あれ?」と思う挙動に遭遇しました。
- タブで
https://foo.example.com/
を開き、SameSite属性なしの Set-Cookie でPHPSESSIDが発行される。 - 別タブで
https://bar.example.com/
を開き、そこから Fetch API (credentials: include
) でhttps://foo.example.com/
にGETアクセスを送ってみる。
→ なぜか初回のfetchでは 1. で発行されたPHPSESSIDが送られない。そのため、このレスポンスで別のPHPSESSIDが Set-Cokie される。 - 上記タブを開いたままで、そこから同じ設定でもう一度
https//foo.example.com/
に fetch してみる。
→ 2. で Set-Cookie されたPHPSESSIDがリクエストで送られる。
まるでタブごとにCookie保管場所が独立しているような挙動を示したのです。*6
CTOに聞いてみて、いろいろ調べてもらったところ「おそらくこれが原因ではないか」という記事を教えてもらいました。
Total Cookie Protection works by creating a separate “cookie jar” for each website you visit.
日本語だと「包括的 Cookie 保護(機能)」と呼ばれているようです:
- 日本語記事
- 英語記事
2022-10-24時点の Firefox 106.0.1 で「プライバシーとセキュリティ」設定を見てみると、以下のようにデフォルトで「包括的 Cookie 保護機能」が有効になっていました。
現時点の Firefox では SameSite未指定時のデフォルトが None 扱いになりますが、包括的 Cookie 保護機能のおかげでクロスサイト Cookie のセキュリティについてある程度担保されているようです。
実験として、あえてこの保護機能を緩めてみます。
トラッキング防止機能の設定を「カスタム」にして、「Cookie」のブロック設定を「クロスサイトトラッキング Cookie」だけにしてみます。
これでもう一度先程の操作を試してみると・・・
https://foo.example.com/
で SameSite属性なしの Set-Cookie でPHPSESSIDが発行される。- 別タブで
https://bar.example.com/
を開き、そこから Fetch API (credentials: include
) でhttps://foo.example.com/
にGETアクセスを送ってみる。
→ 今度は 1. で発行されたPHPSESSID をリクエストで送ってくれました。
整理します:
- 自分の勘違い : Firefoxが SameSite 未指定時に None 扱いになるとしたら、クロスサイトの Fetch API / XHR で Cookie を送るはずだ。
- 2022年10月時点の現実 : 包括的 Cookie 保護機能により、いい感じにクロスサイトの Cookie を分離してくれてる。None 扱い = 全く保護されていない、わけではなさそう。
まとめと感想
CookieのSameSite属性(特に未指定の場合)について「自分がしていた勘違いに対する現実の仕様/挙動」をまとめてみます。
- XMLHttpRequest の
withCredentials
は同じサイトであれば影響しない(常に Cookie を送る) - Chrome の場合は Schemeful Same-Site が導入され、URLスキームも含めて送る/送らないが判断されるようになった。
- Firefox 106.0.1 時点では SameSite 属性が無い場合 None 扱いになる。
- Firefox では包括的 Cookie 保護機能(Total Cookie Protection)があるため、 None 扱いのCookieでもある程度クロスサイトで分離されるようになっている。
感想としては、Cookieの管理が異様に複雑になったなぁ、という感じです。
- 近年のCookie管理は Same-"Site" という名前通り "Site" という考え方で区別するようになりましたが、一方でその前から Cross/Same "Origin" という概念もあって使い分けが混乱しそう。
- Chrome/Firefox それぞれの事情でデフォルト設定が変更されたり戻されたりしていて、キャッチアップが大変。
Ajax全盛時は「マッシュアップ」と呼ばれたように、一つのサイトから Web API を通じて複数のサイトのAPI を XHR を通じて取得・連携するのが盛り上がったこともありました。 しかし 2022 年の現在で同じことをやろうとすると、Origin や Same-Site の概念が立ちはだかり、壊さずに安定して連携する状態を維持するのが難しくなった気がします。
「httpsしか提供しない」「第三者のサイト(API)をフロントから叩かない」など "Origin" や "Site" の違いを気にしなくて済むよう、なるべくシンプルな作りにするとモダンなCookie管理をそのまま受け入れて、開発のしやすさとセキュリティをうまく両立できるのではないかと感じました。
最後に本記事で紹介した各種参考URLは、ほとんど @hasegawayosuke が見つけてきてくれたものです、ありがとうございました!
*1:https://developer.mozilla.org/ja/docs/Web/API/Fetch_API
*2:https://developer.mozilla.org/ja/docs/Web/API/XMLHttpRequest
*3:https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials
*4:https://developer.mozilla.org/en-US/docs/Web/API/fetch
*5:同じサイト間であれば影響しないので、どちらも送るのが妥当そうな気もしましたが、scheme が異なるので扱いが分かれたのだと思われます
*6:「タブごとに」というのはあくまでも今回動かしてみたシナリオの範囲から感じられた表現になります。CTOからは "top level browsing context ごとではないか" というコメントももらっています。