ChromeのCookieポリシー変更と考察
ユーザーのプライバシー意識の高まりに伴い、各サービスも自社のプライバシーやセキュリティ設定の見直しを徐々に始めている。例えば、MacがCatalinaにアップデートされてから、xxxが特定の権限にアクセスすることを許可するかどうか、やたらとしつこく聞いてくるようになったのもその一例だ。
確かにセキュリティ面はより厳格になったものの、そのせいで時として悲劇が起こることもある。ワコムのペンタブが正常に使えなくなったりするのがその例だ。
そしてGoogleも2019年のGoogle I/Oでセキュリティポリシーの調整を発表した。その中で現在のWeb開発に最も大きな影響を与えると思われるのが、Chrome公式によるサードパーティCookieのサポートの段階的廃止だ。
その一環として、Chrome 80以降ではCookieの samesite 属性がデフォルトで lax に設定されるようになる(バージョン80以前のデフォルトはNoneだった)。今回は samesite の定義やその用途、そしてCookieに対する考察を出発点として、この件全体に対する僕の考えを話してみたい。
Cookieとは何か?
Cookieとは、クライアントサイドに実装された小さなストレージ機構(4KB)であり、サーバーから返されるレスポンスヘッダーによって制御できる。これまでCookieを使用する場合、仕組みの実装は基本的にブラウザ側に依存していた。ブラウザが現在の条件や設定されたヘッダーに基づいて、Cookieを送信できるかどうかや、いつ有効期限が切れるかなどを判断していたのだ。Cookieの有効期限が切れておらず条件を満たしている場合、Cookieはすべてのリクエストに自動的に含められて送信される。
ステートレスなHTTPリクエストにおいて、Cookieという仕組みのおかげで僕たちは一部のユーザーデータを保持し、サーバーが現在のユーザーの状態を判断したり、トラッキングを行ったりすることができる。
僕としては、Cookieが最も便利であると同時に最も致命的でもあるのは、まさにこの点だと思う。
Cookieの有効期限が切れておらず条件を満たしている場合、Cookieはすべてのリクエストに自動的に含められて送信される
なぜそう言えるのか?通常、<form>、<iframe>、<a>、<link>、<img> などはデフォルトでCookieを送信するため、以下のようなことが可能になってしまう。
-
iframeを通じたサードパーティトラッキング- Googleアカウントにログインし、google.comがSet-Cookieを返してブラウザ側に保存される
- Bサイトを閲覧する際、Bサイトに埋め込まれたGoogleの
iframeがトラッキングを行う - iframeがCookieをGoogleに送信する
-
<img>を通じたトラッキング- Googleアカウントにログインし、google.comがSet-Cookieを返してブラウザ側に保存される
- Bサイトを閲覧する際、Bサイトがページ読み込み時に
<img src="xxxx.google.com/track/pageview" />で画像リクエストを送信する - CookieがGoogle側に送信されて集計され、現在どのサイトを閲覧しているかが把握される
-
<a>を通じた悪意ある操作- Bサイトにログインする
- このサイトの実装がお粗末で、ユーザー削除のURLが
GET /user/deleteのようになっている - ハッカーからメールが届き、中に
<a href="xxx.com/user/delete">click me</a>というリンクがある - クリックした瞬間、アカウントが削除されてしまう
-
<form>を通じた悪意ある操作- Bサイトにログインしている
- 悪意あるAサイトでフォームを入力するが、実際のリクエスト先はBサイト(例えば決済データなど)になっている
- 決済データがBサイトに送信され、甚大な被害が発生する
3番と4番は、僕たちにとっておなじみのCSRF(Cross Site Request Forgery:クロスサイトリクエストフォージェリ)だ。これに対する防御策は、1. 副作用のある操作にGETメソッドを使わないこと、2. CSRFトークンを使って検証し、リクエストが本当に信頼できる送信元からのものか確認すること、である。
そのため、CSRF攻撃を防ぐ最も一般的な手法はHTML内にCSRFトークンを埋め込み、サーバーが操作を実行するたびにそのリクエストにCSRFトークンが含まれているかチェックして、送信元が自社サービスであることを確認することだ。
確かにCSRF対策によってCookie機構に起因するセキュリティ問題は解決された。しかし経験豊富なエンジニアなら誰もが知っている通り、ステートフルなCSRFトークンの仕組みを実装するのは非常に煩わしく、バグも起きやすい作業だ(特にトラフィックが多い環境では顕著だ)。CSRFトークンの話題が出るたび、みんなうんざりした顔をする。
実は13年以上も前から、<img> や <link> などのタグからCookieを送信させないようにすべきだという提案がなされていた(記事)。しかし、最終的に得られた回答は次のようなものだった。
The attack described here is well-known and called “Cross-site request forgery”. Most believe that it is the web application’s responsibility to fix it, not the web browser’s.
CSRFがもたらす問題を解決するため、Chrome 51以降でCSRF攻撃を防ぐためのSameSite属性が追加された。その原理は、Cookieを送信するかどうかの選択権を開発者に委ねるというもので、3つの値を選択できる:
strict: いかなる状況でもCookieを送信しない。最も安全だが、常に望ましいとは限らない。例えば、BサイトからYouTubeへのリンクを踏んだ場合、Cookieが送信されないため未ログイン状態になってしまう。lax: GETによるナビゲーションで目的のURLに移動する場合にのみCookieを送信する。none: 従来通りデフォルトでCookieを送信する。
Chrome 80+ の Cookie SameSite ポリシー
最近話題になっているSameSite Cookieの変更とは、Chrome 80以降、ユーザーの安全性を確保するために SameSite=none のデフォルト値が SameSite=lax に変更されることを指している。
さて、背景の説明はこれくらいにして、ここからは僕の考察について話していこう。
考察1:SameSite=lax にすればCSRFトークンは実装しなくていいのか?
SameSiteという標準は比較的新しい仕様だ(実際のところ、もうそこまで新しくもないが)。しかし、もしユーザーが古いブラウザを使っていてSameSiteに対応していなかった場合、CSRF攻撃を受けてしまう可能性はないだろうか?
そう考えると、このポリシー変更は僕にとって少し中途半端に思える。サードパーティによるトラッキングを防ぐことはできるが、自社のサービスを守るためには、依然としてCSRFトークンを実装しなければ効果的な攻撃防御はできないからだ。
実際のところ、ブラウザごとにCookieの実装には差異があり、Cookieに起因するセキュリティ問題も少なくない。例えば以下のようなものがある:
- Safari 12より前ではJavaScriptによるhttpOnly Cookieの書き換えが許可されていた
- 仕様が複雑(domain=.example.com と domain=example.com の違いが分かるだろうか?)
- 同一ドメイン間でのCookieの共有
- (今後追記予定…)
要するに僕が指摘したいのは、ブラウザのCookie機構に頼った実装は必ずしも扱いやすいものではないということだ。そこで僕はある別の方法を思いついた。それが次の「考察2」だ。
考察2:もしCookieを極力使わないとしたら?
ネットで少し調べてみたところ、この記事(Cookies Are Bad for You)を見つけた。ここに書かれているアプローチは非常に参考になると思う。
The key is to choose a mechanism that is controlled by the web application, not the browser
Cookieを使うことでブラウザの仕組みに依存せざるを得ないのであれば、いっそのことCookieを使わず、すべての実装をJavaScript側に委託してしまえばいいのではないだろうか?どういうことかというと:
- JavaScript内では
fetchのcredentials: includeを使うことでCookieを送信するかどうかを制御でき、CORSヘッダーと組み合わせることも可能になる - JavaScriptを介することで、CSRF攻撃を効果的に防ぐことができる(詳細は後述)
- 各ブラウザの実装差異に依存しなくて済む
CSRF攻撃を防ぐ方法として、ユーザー認証が必要なあらゆるリクエストにAuthorizationヘッダーなどのリクエストヘッダーの付与を必須にすることが挙げられる。これによりCSRF攻撃を防ぐことができ、CSRFトークンの仕組みをわざわざ実装する必要もなくなる。
フォームの送信時もブラウザネイティブの機能には直接頼らず、JavaScriptのAPIを使って行う。
const form = new Form()
form.append('keyA', 'valueA');
fetch('/my-api', { body: form, method: 'POST', headers: { 'Authorization': 'xxx' } })
ただし、JavaScriptを利用するアプローチでは、有効期限の管理やリクエスト送信を自前で実装しなければならない点に加えて、以下の点を考慮する必要がある:
- データをどこに保存するか?
- XSSが発生したらどうするのか?
- ユーザーがJavaScriptを無効化していたらどうするのか?
データをどこに保存するか?
データ(アクセストークン)はメモリ内、localStorage、sessionStorage、あるいは IndexedDB に置くことができる。最初は「XSSに遭ったらどうするんだ?」と違和感を覚えるかもしれない。その点について引き続き見ていこう。
まず大前提として、クライアントサイドにはセンシティブな情報を置きすぎないことだ。また、アクセストークンの有効期限も極力短く設定し、トークンが漏洩した際の影響を最小限に抑える。その上でリフレッシュトークンの仕組みを活用してユーザー体験を担保する。
XSSはどうするのか?
僕に言わせれば、どんな仕組みにも一定のリスクはつきものだ。Cookieであっても過去にセキュリティ脆弱性が発覚したことがある。昨今のフロントエンドフレームワークの助けを借りることで、大部分のXSSの脆弱性はすでに回避できているとも言えるのではないだろうか。
ユーザーがJavaScriptを無効化していたらどうするのか?
JavaScriptを無効化された場合の対応については、割り切りが必要だと僕は思う。Facebook、YouTube、Netflixを見てほしい。これらのサービスはいずれもJavaScriptの有効化を求めているではないか。
スクリーンリーダーに関しては、より良い体験を得るために基本的なJavaScriptを取り入れるスクリーンリーダーが今後増えていくのではないかと考えている。最近のトレンドとしてJavaScriptのサイズが数百KB単位と肥大化しがちではあるものの、スクリーンリーダー対応にせよアクセシビリティ対応にせよ、JavaScriptを用いることでより繊細な制御を提供できるようになる。
では、そうなるとimgやformなどもすべてJavaScript経由でAPIを叩くべきなのだろうか?SPAが主流となった現在、XHR経由でAPI送信を行うサービスはすでに増えている。JavaScriptが必須となり実装の手間が少し増えることを除けば、より信頼性の高いセキュリティが手に入るのだと僕は思う。
考察3:OAuth
これは記事の中でも触れられていた点だ。
OAuthプロトコルを利用することで、認可サーバーとトークンを交換し、クライアントサイドにトークンを保存して利用できる。そしてHMACアルゴリズムを併用することで、リクエストメソッドやリクエストURLが正しいことを検証できる。
結論
セキュリティと利便性は、本来表裏一体のものだと僕は感じている。すべての実装、検証、有効期限管理などをサービス側に移すのは確かに骨が折れる。以前は僕もCookieは安全で使い勝手の良いものだと思っていたが、今回のポリシー変更とこれまでに目にしてきた大小様々な事例を通じて、改めて考え直させられた。この記事の中にはまだ考えが至っていない視点もあるかもしれないので、ぜひ意見を聞かせてほしい。
関連記事
- Three.js で僕の部屋を表現する 僕が React Three Fiber を使って実際の自分の部屋をブラウザ上に再現し、実世界のオブジェクトを目録に見立て、空間の記憶を通してここ数年の生活と仕事について語った話。
- フロントエンドで画像を扱う際に注意すべきこと Jake Archibaldの記事を起点に、現代のレスポンシブ画像の書き方を整理する。なぜwidth/heightを付ける必要があるのか、CSSのaspect-ratioはいつ使うべきか、AVIFとWebPの選び方、そしてpicture/source/srcsetを使ったモバイル向け画像の切り替えについて。
- CSS field-sizing — たった1行のCSSでフォーム要素を自動リサイズする かつてtextareaの自動高さ調整は、JavaScriptでscrollHeightを監視するしかなかった。しかしCSSのfield-sizing: contentなら、わずか1行で代替でき、textarea、input、selectに対応している。本記事では従来のやり方のペインポイントと、field-sizingの使い方をまとめる。
- リンクの下線をもっと見栄え良くする:text-underline-offset デフォルトでは下線と文字が近すぎて、このスタイルを好まないデザイナーもいるし、僕自身もあまり綺麗ではないと感じていた。