開発者はCORSを理解していない(2019)
(fosterelli.co)- ZoomのローカルWebサーバーの脆弱性は、多くのWeb開発者が CORSの動作方式 を誤解しているとき、セキュリティ境界がいかに簡単に崩れるかを示している
- Zoomは
localhost:19421のローカルサーバーと通信しながら、AJAXの代わりに 画像サイズ でステータスコードを伝えており、これはCORSを回避しようとする迂回実装と解釈できる - ChromeはlocalhostのWebサーバーにも CORSヘッダー を適用し、異なるlocalhostポート間のフロントエンド・バックエンド通信もブラウザでサポートされている
- より安全な設計は、ローカルサーバーがREST APIを提供し、
Access-Control-Allow-Originを設定して zoom.usのJavaScript だけがアクセスできるよう制限する方式である - 同一生成元ポリシーを回避すればコードは動くかもしれないが、ローカルサーバーの権限ある機能がインターネット上のあらゆるWebサイトに公開されうる
ZoomのローカルWebサーバーが生んだCORS回避
- フルスタックコンサルティングの現場でさまざまな規模や業界の開発者に接する中で、Web開発者が CORS を理解していない問題が繰り返し見られた
- 最近のZoom脆弱性で、セキュリティ研究者Jonathan Leitschuhは、Zoomがユーザーのマシン上で
http://localhost:19421Webサーバーを起動していることを発見した- ユーザーがZoomリンクを開くと、ZoomのWebサイトがlocalhost Webサーバーにリクエストを送り、ネイティブのZoomアプリを実行する
- 通常のAJAXリクエストの代わりに、ローカルのZoom Webサーバーから画像を読み込み、画像の異なるサイズでサーバーのエラー・ステータスコードを表現していた
- ブラウザがlocalhostサーバーのCORSポリシーを無視するという解釈は誤りであり、Chromeはlocalhost Webサーバーの CORSヘッダーを尊重 する
- Create React AppのフロントエンドとバックエンドAPIを異なるlocalhostポートで実行するときもクロスオリジンリクエストが発生し、これはすべてのブラウザでサポートされている
- AJAXリクエストがブロックされたため、Zoomは画像ハックでCORSを回避したように見える
- その結果、ZoomのWebサイトだけでなく、インターネット上の他のWebサイトもネイティブクライアントの動作をトリガーし、応答にアクセスできるようになった
安全な代替策と残るUXの問題
- 安全な実装は、
localhost:19421のWebサーバーが REST API を実装し、Access-Control-Allow-Originヘッダーの値をhttps://zoom.usに設定する方式である- こうすれば、zoom.usドメインで実行されるJavaScriptだけがlocalhost Webサーバーと通信できる
- zoom.usは、バックグラウンドでZoomミーティングが自動的に開くのを防ぐため、iframeレンダリングをブロックする Content Security Policy ヘッダーを置くこともできる
- どのページでもブラウザをzoom.usのミーティングリンクへリダイレクトできる問題は依然として残る
- ただし、これはソフトウェアの脆弱性というより、Zoomが選んだ ユーザー体験 に近い
- リンクをクリックしたとき、カメラとマイクが見知らぬ相手に突然開かれないだろうというユーザーの期待をZoomは裏切っている
- ブラウザ標準のポップアップをUX上の理由で避けたいなら、アプリ内でポップアップを表示することもでき、Google Meetはその方式をうまく使っている
- localhostでWebサーバーを実行すること自体が危険な試みであり、特に ソフトウェアのインストール のような権限ある機能をインターネット上のあらゆるWebサイトに提供してはならない
- CORSはこうした状況を安全に処理するための仕組みであり、回避すべきではない
Zoomだけの失敗ではないCORSの混乱
- Zoomが実際にCORSを理解していなかったためにこの方式を選んだのかは確かではない
- Redditの lerunicorn は、Firefoxが安全なオリジンから安全でないオリジンへのXHRをブロックする可能性があると見ている
- しかしFirefoxは、originがlocalhostの場合にはこれをサポートしている
- ネイティブアプリは固有の自己署名証明書を生成でき、ブラウザ拡張 を使うこともできる
- いずれの場合でも、オリジンのフィルタリング を省略する正当な理由にはならない
- CORSの混乱はZoomだけの問題ではない
- Stack Overflowには
Access-Control-Allow-Origin関連の質問が 数多く存在 する - Expressの例の中には、そのままコピーするとアプリケーションが脆弱になりうる 安全でないデフォルト を勧めるページもある
- 他のベンダーもZoomと 同じ脆弱性 を経験したことがある
- Stack Overflowには
- 開発者はコードを動かしたいと考えるが、同一生成元ポリシーを丸ごと回避すると、Zoomの事例のようにローカル権限が外部のWebサイトに露出する
- CORSの混乱は熟練開発者にも新人開発者にも見られ、CORS APIが過度に複雑なのか、あるいはCORSとCSPの教育が不足しているのかは明らかではないが、現在のやり方はうまく機能していない
1件のコメント
Hacker Newsのコメント
TFAもCORSを正しく理解していないか、かなりひどく誤って説明しているように見える
Access-Control-Allow-Origin: https://zoom.usは、zoom.us ドメインの JavaScript だけが localhost サーバーと通信できることを保証するものではない。他のウェブサイトの JavaScript もlocalhost:19421に同じようにリクエストを送ることはできる。CORS は何かを制限するものではなく、既定の制限を緩和する仕組みだ。このヘッダーがするのは、zoom.us で実行される JavaScript がlocalhost:19421のレスポンスを読めるようにすることだけであり、リクエスト自体はどうせ発生するのだから、バックエンド側で副作用が起きないようにしなければならないGET リクエストは送られるが、本来は冪等であるべきなので、サーバーが正しく実装されていれば副作用を起こせないはずであり、GET ではレスポンスを読めるかどうかが重要だ。逆に、副作用を持ちうる非冪等リクエストでは、クロスオリジンの状況で実際のリクエストの前にまず preflight OPTIONS リクエストが送られ、OPTIONS レスポンスに正しいヘッダーがなければ実際のリクエストは送信されない
CORS に関する誤解があまりにも広く распространし、文書もたびたび互いに矛盾しているので、見知らぬ相手が正しく実装していると期待するのは難しい。あるプロトコルがこれほど広範な混乱を生むなら、一方が正しく動いていても他方もそうかはわからない。人々が別の実装に合わせてコードが動くまで修正してきたのなら、間違っているのが自分側なのか相手側なのかも曖昧になる
たとえば
Content-Typeがtext/jsonの POST は、OPTIONS preflight なしでは第三者ホストに送れないが、multipart/form-dataの POST は許可されており、CORS では防がれない。そしてエンドポイントがContent-Typeを厳密に確認せず JSON だと仮定しているなら、どのウェブサイトからでもユーザー操作なしに POST を送れることになるまともなウェブ開発者なら GET/HEAD/OPTIONS で状態を変更させてはいけないし、会議への参加のようなものは状態変更だ。PUT/DELETE も冪等であるべきだ。JSON またはフォーム以外の形式の POST API は
Content-Typeヘッダーを確認すべきであり、PUT/PATCH/DELETEとフォーム形式でないContent-Typeの POST は preflight を引き起こし、実際のリクエストがサーバーに到達する前に CORS が確認される証明書を作るだけでは動作せず、マシン上のすべてのブラウザ信頼ストアにルート CA 証明書としてインストールされていなければならない。ルート CA の秘密鍵が適切に保護されていなければ、どのウェブサイトでも中間者攻撃ができてしまうので、少なくとも名前制約が必要だ(https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.10)。ところが Chrome では 2023 年の v112 以前まで、ルート CA ではこれが機能せず(https://alexsci.com/blog/name-non-constraint/)、そのため中間 CA を追加してそこに制約をかける必要があった。もちろんルート CA の鍵は破棄するのが正しい
以前、ローカルのルート CA を使うプロジェクトで基本制約を追加したことがあるが、ルート CA に誤って入れてしまい、しかも全ブラウザでテストしていなかった
もっと多くの人が MDN のCORS ドキュメントを読めばいいのにと思う。CORS を理解しようとするときにとても役立ったし、ここのコメントを見るまで人々がここまで苦労しているとは知らなかった
https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
理解が難しいのは CORS だけではなく、多くの開発者が脅威モデルをきちんと理解していない
説明を聞いても、なぜそれが大きな問題なのか直感的につかめないことが多い。特にバックエンド開発者が CORS を設定することが多いが、CORS はアクセス権限を保護する仕組みではないので、バックエンドの立場からはあまり重要に見えない。攻撃者は持ち出せないと感じるし、フロントエンドの立場では面倒な障害物のように見えやすい。この記事は具体例をうまく示している
運用担当としてロードバランサーであらためて正しく直し、少なくともアプリケーションはいまは動いている。CORS は理解しにくいが、CORS が防ごうとしている脅威モデルだけでなく、ウェブ開発全般、とくに HTTP プロトコルを理解していない開発者も多いというのがさらに残念だ
multipart/form-dataはよいのに、アプリケーションの JavaScript はだめ、というようなものだCORS は任意であり、他のライブラリやツールは単に無視することもできる。CORS が実際に意味を持つのは、ログイン済みの人間ユーザーに対する XSS と CSRF を防ぐ場合だけで、それ以外の攻撃シナリオではどうせ HTTP ヘッダーを偽装するスクリプトやプログラムを使うので無意味だ。だから人々は結局すべての CORS オプションを有効にしてしまうのだが、これは XSS と CSRF を許してしまう最悪のケースだ
このコメント欄は本当に情報レベルが低く見えるし、むしろ筆者の要点をそのまま証明している
CORSが生まれる前にWeb開発をしていたなら、もともとクロスドメインリクエストは禁止されていて、CORSはこの制約を回避するために生まれたことを理解している。だから、やりたいことを実現するにはCORSを有効にすればいいと受け止めやすい。
逆にCORS以後にWeb開発を学んだ人は、クロスオリジンリクエストを試し、ブラウザが許可されていないと判断し、CORS preflightを試し、失敗するとコンソールにCORSエラーが出る、という流れしか見ない。内部動作を知らずドキュメントも読まないまま推測すると、CORSがリクエストを止める原因だと思い込み、「CORSを無効化」しようとするようになる。だが、CORSは問題の原因ではなく解決策だ。
同じ誤解を持つ人たちがチュートリアルやオンライン議論で自信満々に繰り返すので、さらに混乱する
https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
コメントを読んで、自分だけではないと確認できた。誰もCORSを理解できない理由は、複雑すぎて衝突が多いからだ。
標準やヘッダーも変わり続けるので、開発者はたいてい動くまであれこれ触ってから製品をデプロイして終わる。動いていても開発者コンソールにエラーや警告が残ることがあるが、表向き問題なく動いていればそのまま触らなくなる
CORSを理解するには、まず同一オリジンポリシーを理解する必要がある
特に「なぜこれが必要なのか?」が難しいなら、ここから始めるとよい: https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Same-origin_policy
以前、同一オリジンポリシーを面接質問に使ったことがあるが、多くの応募者がなじみがなく、その質問から得られる情報は少なかった
Webアプリを開発してきたなら、いつかは同一オリジンポリシーに直面していたはずだからだ。知らないなら、たとえばバックエンドとどう通信していたのかなどをさらに聞くことになる。CORSの問題に遭遇したが最短の回避策だけ適用して忘れたのか、実際に理解しようとしたのかも、一部の役割では有用なシグナルになる。
バックエンド職にはあまり向いていない。すべてのバックエンド開発者が、CORSの問題に頻繁に遭遇するフロントエンドチームと密接に働いていたわけではないからだ
CORSについて覚えているのは、デバッグに予想よりはるかに時間がかかり、ブラウザのエラーメッセージが意図的に乏しく、CORSエラーがほかの失敗モードと最初は区別しにくいということだ
もちろん、サーバーがCORSリクエストを理解できずおかしな応答を返すと、それが最終的にCORS失敗として表れることはある
コメント欄を見ていてかなり面白かったので付け加えると、同一オリジンポリシーはブラウザがアクセス権のないWebサイトへ情報を漏えいさせないよう保護し、CORSはその保護を弱められるようにする
たとえば同一オリジンポリシーは、
example.comがyoutube.comの購読リストを取得できないようにする。だがCORSを使えば、example.comがyoutube.com/public/*にはアクセスできるよう許可できる。もう一つの用途として、バックエンドAPIが別のフロントエンドの配下で動いてデータ窃取につながるのを防ぐ効果もある。たとえば実際のサービスにはログインしているが、ユーザーは
g00gle.comにいて、すべてのリクエストが中間者攻撃されうる状況を防げる自分もその一人だ。CORSは定期的に勉強し直さなければならない話題で、いつも忘れてしまい頭に定着しない。
バックエンド開発者なのでCORSの問題にほとんど遭遇しないからだと思う。毎日使わないものは忘れやすい
まともな世界なら、エラーメッセージに「レスポンスヘッダー」や「metaタグ」といったヒントが入っているはずだが、主要ブラウザベンダーは謎めいたメッセージを書く人たちを雇っているように見える。Chromeの “requested resource” はまだましだが、それでも暗号のようだ。
より良いメッセージは、たとえば
https://bank.comのリソースがCORSヘッダーを持たないためクロスオリジンリクエストを許可しないとか、現在のオリジンがCORS許可リストに含まれていない、といったものになるべきだ。ネットワークタブのpreflightリクエストやMDNへのリンクも併せて表示すべきだろう。CSPも、このページのCSPヘッダーのためリソースを取得できず、インスペクタのページリクエストヘッダーやmetaタグへつなげる形のほうがよい結局のところ、たいていはサーバーが改変されていないブラウザリクエストからのみアクセスされるという想定に依存している。Zoomの脆弱性は、クライアント側でCORSやCSPを回避するのがあまりに簡単だったために生じたし、Zoomが悪く、怠慢で、愚かだったのはその通りだが、こうしたモデルを維持し続けるコミュニティにも責任があると感じる
同一オリジンポリシーが、ブラウザが悪意あるスクリプトを実行して情報を漏えいさせるのをどう防ぐのかは理解している。
Access-Control-Allow-Originヘッダーでサーバーが追加のオリジンを信頼すると宣言し、SOPを緩和することも理解している。それでも
Access-Control-Allow-Headersヘッダーの目的はまだ分からない。ブラウザのセキュリティを改善するようにも見えないし、ましてサーバーのセキュリティでもない。プロトコル設計者が「完成度のために」入れたのか気になる。関連: https://stackoverflow.com/questions/17992042