記事の目的と想定読者
World Wide Webが世の中で比較的広く使われるようになって、すでに20年を超えました。アクセス元の主役もPCからスマートフォンになり、Webはシンプルを追求する設計思想から大きく幅を広げてきました。この記事は、主にサイトコンテンツ開発者が安全で便利なサイトを開発するための基礎知識を身につけるために通して読んでいただくこと、あるいは深く調べるためのネタ元として役立てることを念頭に置いて書いています。冗長に感じられることも多いと思いますが、お許しください。
なぜSame-Origin Policyがあるのか
Same-Origin Policyって何?という話はWikipediaにお任せするとして、なぜSame-Origin Policyが必要なのかという話は根本的理解に必要不可欠なので、今さら感はありますが、ちゃんと書いておきます。
そもそもWorld Wide Webはハイパーテキストを使って、インターネットの情報を縦横無尽に行き来をしたり、埋め込んだりするもの。今でも原則的には、クロスドメインの画像などをページに埋め込むことは自由1であり、実際に広く行われています。
認証付コンテンツ
インターネットがオープンなコンテンツだけで構成されているなら、Same-Origin Policyは必要ありません。誰が取得しても同じコンテンツならば、安全面に違いはなく、Same-Origin Policyは利用者自身の安全には寄与しないのです。
でも、今や多くの人がブラウザーでメールや、クラウド写真ストレージサービスを使っています。例えばブログページのHTMLタグやスクリプトから簡単に(gmailなどクロスドメインの)メール本文やプライベートな写真を取得して、ブログサーバーにPOSTしてしまうことができるとしたら、とても恐ろしいことになります。取得できなかったとしても、削除したり改変したりできたら、大変なことになります。
このように、Same-Origin Policyは利用者とサービス提供者の間でプライバシーを守るためにあるというのが一義的な存在価値です。そこで重要なのは、認証付コンテンツを他のページから簡単に抜き出せないこと。そしてコンテンツに対する操作を簡単に実行できないことです。
クロスサイトリクエストの必要性
そもそもコンテンツを別サイトから取得する必要はあるのか?という疑問もあると思います。結論から言えば、現代のWebコンテンツにおいては、ほぼ不可欠と考えて良いと思います。
- CDN (Content Delivery Network)を通した最適経路の画像や埋込動画
- 同様にCDNを通した最新版のJavaScriptライブラリー
- 同様にCDNを通した最新デザインのCSSやWebフォント
- Google Analyticsなどの統計処理
- 「いいね!」の数などを表示するSNSボタン
- 自社ブランドのECサイトからの共通ショッピングカート利用
- 行動ターゲティングなどを含む広告
- 各種クラウドAPIとの連携
こうしたコンテンツを共通のオリジナルを保有するサーバーから取得したり、大量のトラフィックを物理的に有利なサーバーから取得したりすることは理にかなっています。むしろ単一のサーバーから取得するのは、もはや非現実的と言って良いでしょう。
しかし、これらのしくみがセキュリティー上の脆弱性や、プライバシー漏洩の温床になっていることも事実です。Same-Origin Policyはそうした問題からコンテンツを守る基礎になるものです。
特にここ数年、プライバシーに対するWebの考え方は変わってきました。行動ターゲティング広告などは、その存在自体がユーザーのプライバシーを侵害するものであると考える傾向も強くなっており、上記のようなすべてのサービスパターンが今後も長期にわたって維持されるとは考えにくい時代になりました。サイト開発の際には、本当にクロスサイトで取得することが必要なのか、もう一度考え直すことも必要になってきています。
Same-Origin Policyの限界
Same-Origin Policyによってコンテンツを守るという考え方は、限界ではないか、別の枠組みが必要なのではないか、という議論は長らく存在しています。
多くのWebエコシステムが、Same-Origin Policyによる保護を前提に組み立てられている以上、短期間に変わることは難しいのですが、いずれ大きな変化が来る可能性はあるのかもしれません。
問題の多くは、Same-Origin Policyがドメイン名を隔離の根拠としている点にあります。ホスティングサービスやクラウドサービスにより、異なるテナントが同一のドメインを共有することがありますが、こうしたサービスからは事実上Web Storageなどを使うことができません。他テナントのコンテンツから丸見えになり、書き換えもできてしまうためです。
逆にCDNを活用して積極的にドメイン分離を進めるサービスもあります。例えばHTMLと動画のドメインを分離することで、動画への集中アクセスが発生しても、HTMLページの応答性能への影響を避けることができます。このような場合に認証情報を共有するためにはSame-Origin Policyを正しく理解し、Open ID ConnectやSAMLなどのシングルサインオンを正しく理解する必要があります。
Web上での情報追跡の基盤とも言えるCookieは、基本をSame-Origin Policyとしながらも、pathやdomainといった要素があります。ドメインを集中させる方向と、ドメインを分離させる方向、両方のパターンについて配慮した設計と言えますが、これが事態を複雑にしている面もあります。
インターネットにはドメインの管理体を認定する安全なしくみが現存しないため、そもそもドメインという単位はそれほど確実でも安全でもありません。管理体の証明に一番近いのはEV SSL証明書ですが、異なるドメインの管理体や、ドメイン内での管理体制や責任はわかりません。
こうした課題を補完するために、後述するPublic Suffix Listのような付け焼き刃的な機能も使われているし、Same-Origin Policyを回避するための手段も多数用意されています。原則は双方(複数オリジン)の合意があれば、回避する手段はあると思えば良いのですが、悪意が入り込むことを排除するために、しくみはとても複雑になっています。
なるべく全体像を把握できるように、Same-Origin Policy周辺を総合的に記述していきたいと思います。
Cookieとプライバシー
Same-Origin Policyの制限について正しく理解するには、何を防ぎたくてこの技術仕様に至っているのかを理解することが重要です。話をスムーズにするために、ここで少しCookieのことに触れます。
Cookieには、技術仕様ではなく、使い方による分類があります。一般にファーストパーティーCookie、サードパーティーCookieと呼ばれるものです。両者にプロトコルやフォーマットの違いはありません。あるCookieが、ファーストパーティーCookieなのかサードパーティーCookieなのかをプロトコルや値から判定することはできないのです。
ファーストパーティーCookieとは、ユーザーが自分で見ていると認識しているWebサイトによって発行されたCookieです。認証をはじめとするページ間連携を実現するためにとても重要な技術要素でもあり、必要不可欠でもあります。
サードパーティーCookieというのは、ユーザーが見ているWebサイトではない、別のサーバーから発行されるCookieです。こちらもインターネット広告や閲覧統計の収集などに幅広く使われていますが、ユーザーからその実態が見えにくく、いつの間にか数多くのサイトで共有されている存在です。このため近年、サードパーティーCookieに対する視線は年々厳しさを増しています。
2021年を迎える現在、主要ブラウザーではFirefoxとSafariがデフォルト設定でサードパーティーCookieをブロックしています。Webページに埋め込まれた画像やスクリプトを取得するリクエストに対する応答でSet-Cookieすることはできません。しかし一方で、適切な設定をすれば、ファーストパーティーCookieとして発行されたCookieは、Webページに埋め込まれた広告を通して広告サーバーに送信され、サードパーティーCookieの代わりを務めることができます。
Appleは2017年にリリースしたSafari 12からITP (Intelligent Tracking Prevention)を導入しました。サードパーティーCookieをセットできないだけでなく、ファーストパーティーCookieであっても、ユーザー追跡に使われていると判断されたものは24時間でサードパーティー送信されなくなります。さらに、ファーストパーティーCookieであっても30日間で無条件に完全に削除されます。
ユーザーが知らない間にファーストパーティーCookieを発行するための、first party bouncerと呼ばれるリダイレクトページをリンク間に挟む方法も検出され、行動追跡のためのCookieと判定されるようになっています。さらにはlink decorationと呼ばれるパラメーター付与で行動を追いかけるしくみまで制限がかけられています。
このようにCookieを取り巻く環境は厳しいわけですが、その一方でsupercookieと呼ばれる、Cookieを使わずにユーザーを追跡するためのテクニックも生み出されています。
シークレットウィンドウや、In-Privateブラウズなどのメニューから検索したのに、どうも行動ターゲティング広告に反映されているような気がしたことはないでしょうか。ブラウザーのキャッシュ、フォント、HSTSなど、あらゆる情報を使ってユーザーを識別する手段が考えられています。デバイスフィンガープリンティングなどとも言われます。
ITPなどのブロック技術はこうした新しい手法に対しても対応が広がりつつあります。具体的には、例えば同じページでもリンク元によってブラウザーのコンテンツキャッシュを分離するなどの対策がとられています。
セキュリティー問題の実情
タイトルに「2021年」を入れたのは、セキュリティー関連の問題は時代によって刻々と状況が変わるからです。いつの話なのか?を入れずに正しく議論することはできません。ブラウザーの実装や想定される問題も、時間軸とともに少しずつ変化があります。
そのあたりも含め、まずは現在のSame-Origin Policyを構成している要素を列挙してみましょう。
クロスサイトスクリプティング(XSS)
Same-Origin Policyはコンテンツのプライバシーを他サイトから守るために重要ですが、裏を返せば、Same-Originでさえあれば何でもできるということでもあります。XSSとは、何らかの方法で攻撃者がOriginのサイト上でJavaScriptを実行させることで、Same-Origin Policyの制限を回避することです。
このため、コンテンツそのものをWebからのアクセスで更新するCMS (Content Management System)は、XSSの標的になりやすい。もともとコンテンツを書き換えるためにあるので、その中にJavaScriptを紛れ込ませてしまう特殊な方法が見つかりやすいのです。
Same-Origin Policyが保護の基本的な考え方になっている以上、XSSは重大な問題になります。Same-Origin Policyを正しく理解するためにもXSSへの理解は欠かせないので、少しXSSに関連した設定などについて書きます。
X-XSS-Protectionヘッダー
古くIE8の頃に導入されたXSS防止機能に関連したHTTPヘッダー。XSS対策されたブラウザーでは、サーバーに送信したパターンとページ内容から、XSSを推定し、XSSと判断した場合は、コンテンツ内のスクリプトの実行を自動的に抑制します。
XSS対策が実装される前に作られたWebアプリケーションの中には、このXSS防止機能によって動作しなくなってしまうものがあるかもしれません。(でもおそらくそれはXSSリスクの高いアプリケーションです)
そういった場合にX-XSS-Protectionヘッダーを0に指定することで、XSS防止機能を無効化することができます。Same-Origin Policyとは直接関係ありません。
基本的にはX-XSS-Protectionヘッダーは使わないのが現在では正しい対応です。
Content-Security-Policyヘッダー (CSP)
XSSに対するもっとも包括的な対策がCSPです。CSPによって、ページから読み込むクロスサイトコンテンツの種類ごとに許可するドメインをSame-Origin Policyに制限したり、直接指定したり、拒否したりすることができます。
また、オリジンを制限できるだけではなく、それらクロスサイトコンテンツの読み込み後の動作を制限することもできます。
Same-Origin Policyと直接は関係ないのですが、CSPについて覚えておいた方が良いのが、report-uriを指定できる機能です。これを使うと、CSPに反するスクリプトの動作やコンテンツの読み込みがあったことをサーバーに送信することができます。事前のテストでは発見できなかった脆弱性や、予期しない使い方によって発生する問題を記録できることになるため、未知の課題を発見する手段になります。
セキュリティーリスクを最小化するという意味で、手間はかかりますが、default-src self; sandbox allow-scripts; report-uri https://(your site)
といったあたりをスタートラインとして、クロスドメインのコンテンツや広告などの存在に応じて緩めて行くという方法を推奨します。
クロスサイトリクエストフォージェリー (CSRF)
XSSのような仕掛けをして任意のJavaScriptを動作させることに成功すれば何でもできます。しかし、より狭い範囲でSame-Origin Policyによるセキュリティーを悪用しようとすれば、URIひとつで十分とも考えられます。
「写真ストレージの全データを削除するURI」「クラウドアカウントを削除するURI」「自分のメールアカウントから特定の相手に誹謗中傷のメールを送信するURI」といったものがサービスに存在するならば(実際にその機能がある以上、それを実行するURIは確実に存在します)ブログやメールやSNSを通して、利用者にそのURIにアクセスさせるだけで、大きな被害を発生させることができるのです。
CSRFを防止するには、そうした機能に割り当てられるURIが常に同一ではなく、期限のある認証トークンを含んでいること、あるいはURI以外の要素(Cookie, POSTデータ, カスタムHTTPリクエスト・ヘッダー)が認証トークンを含んでいることが重要です。具体的な対策方法は直接はSame-Origin Policyとは関係なく、複雑なので、ここでは割愛します。
SameSite Cookie
CSRF対策のひとつとして、Chrome 51, Firefox 60といった比較的最近のバージョンで盛り込まれた仕様にSameSite Cookieがあります。当初はCSRF対策としてCookie情報を守るための設定という位置付けでしたが、Chrom 80, Firefox 69からはSameSiteの省略時の値がLaxに変更となり、認証付きのCORSをするためにはSet-CookieにSameSite属性を指定することが必要になりました。SameSiteのデフォルト値を解除するためにNoneを指定する際には、Secure属性も必須となっています。つまり現在のブラウザーではCORSにはhttpsが必須条件です。
画像、CSS、JavaScript、iframeコンテンツなど、一般にHTMLの記述だけでもクロスサイトで取得することができるコンテンツは複数あります。これらコンテンツをトップレベルのフレームと異なる場所(CDNなどは実際によく使われます)に置いた場合、まずcrossorigin属性を書かなければ、これらの取得時にCookieを送信することはありません。広告はユーザーが誰なのかを認識したいために、明示的にcrossorigin属性を書くことでサードパーティーCookieをトラッキング情報として利用することが広く一般的に行われています。意図して広告を利用したい場合、広告サーバー側はSet-Cookieの際にSameSite=None; Secureを付加すれば良いので、対応は難しいものではありません。
それでは、なぜブラウザーはSameSite=Laxをデフォルト値としたのでしょうか。これは、Webサイトが正しいCSRF対策をすることが意外に難しいことが主な理由と考えられます。一意的なCSRF対策は攻撃者が推測不可能なトークンをWebリクエストに追加することで、正規のページを通したリクエストであることを検証できれば良いのですが、これにXSS脆弱性が加わると、攻撃者が正規のリクエストを生成できてしまうことがあります。SameSite=Laxは、Cookieの送信を、そもそもユーザーがトップページとして認識しているサイトにおけるリクエストに限定することで、攻撃者からのCSRFを根絶することを目指しています。一方で、ECサイトのショッピングカートや、行動ターゲティング型の井Web広告など、正規のクロスサイトリクエストを必要とするサービスはあり、これらは厳しい要件を満たした上でSameSite=Noneを明示的に指定することが求められます。
Timing Attack
攻撃者は自分のサイト内に勝手にトップレベルドキュメント(HTML)を作成し、例えばiframeを使って攻撃対象のコンテンツをフレームの中に呼び出すことができます。もちろんSame-Origin Policyによって、フレーム内の情報を直接読み取ることはできないのですが、さまざまな手法を用いて内容を推測する可能性はあります。
実際にこのような攻撃ではpixel perfect timing attackと呼ばれるものがあります。コンテンツの中身が見えなくても、処理にかかる時間を正確に測定することで、内容を推測できる場合があります。後述するResource Timing APIでは、Same-Originではないコンテンツの取得時には、これを許可するTiming-Allow-Originヘッダーを出力しない限り、タイミング情報を取得できないようになっています。
Clickjacking
CSRFと似ている攻撃手法として、Clickjackingがあります。利用者には正規の操作をさせるために、見た目を偽装して正規の危険な操作に導く点が違います。Clickjackingには多くの手法がありますが、そのひとつにiframeを使って正規のコンテンツをページ内に埋め込んで偽装ページを作る手法があります。
X-Frame-Optionsヘッダー
Same-Origin Policyと関連するHTTPヘッダーとして、主にClickjacking対策として使われるX-Frame-Optionsヘッダーがあります。
X-Frame-OptionsはX-XSS-Protectionと違い、指定しなければ安全対策は働かず、指定することで安全を向上させることができるヘッダーです。
X-Frame-Optionsヘッダーに指定できる値のひとつにSAMEORIGINがあり、ここでSame-Origin Policyが適用されます。これを使うことで、Same-Origin Policyに合致しないページをiframeの中に閉じ込めることができなくなり、Clickjackingのひとつの可能性を抑止することができます。
Cross Site Script Inclusion (XSSI)
名前はXSSと似ていますが、根本的に異なる攻撃です。scriptタグがクロスサイトで使えることを利用し、scriptタグで(主にスクリプト以外の)リソースを読み込みます。
多くはスクリプトエラーになり、スクリプトとしては実行できない場合が多いでしょう。このときのエラーの内容を読み取ったり、スクリプト評価のしくみを悪用して、リソースの中身を推測する攻撃がXSSIです。
有名なXSSIの攻撃に、Arrayクラスのコンストラクターを再定義した上で、JSONをscriptタグで読み込ませるJSON Hijackingがあります。
XSSIを緩和するために、モダンブラウザーではクロスサイトのscriptタグがエラーになった場合、エラーの詳細を報告せず、単にScript Errorと報告されます。
また、XHRやfetchなどを使ってJavaScriptからJSONリソースを取得する場合、サーバーは素直なJSONではなく、)]}'
のprefixを持つJSONを返す対策は広く利用されています。この場合、scriptタグで読み込もうとしても必ずエラーになりますが、JavaScriptから実行した場合には先頭4文字を削除してJSON.parse()すれば良いために、問題になりません。
X-Content-Type-Optionsヘッダー
XSSIを防止するためにサーバー側ではX-Content-Type-Options: nosniffを指定することができます。この指定がある場合には、ブラウザーは厳格にContent-Typeヘッダーで指定されたMIME-Typeを解釈します。scriptタグに対して、javascriptではないものが応答された場合、データは無効になり、スクリプトエンジンには渡されません。
もともとWebではMIME-Typeがリソース種別の判断基準であるはずなのですが、レンタルサーバーの台頭や知識不足のサーバー管理者の増加で、正しくMIME-Typeを設定することができないサイトが実質的に増えてしまい、現在のような状況になってしまったと言われています。X-Content-Type: nosniffはこれを本来の状況に戻すように働きます。IE8を含む主要なブラウザーはすべて対応しています。
X-Requested-Withヘッダー
jQueryやAngularなどがJavaScriptからXHRでサーバーにリクエストを投げる際に、X-Requested-With: XmlHttpRequestヘッダーを付加します。
これはIETFやW3Cで標準化されたものではありませんが、サーバー側ではこのヘッダーでscriptタグからのリクエストではないことが確認できます。scriptタグでブラウザーがリソースを取得しに行く際のHTTPリクエスト・ヘッダーにX-Requested-Withを追加することはできないからです。
ただ、どのようなJavaScriptからのリクエストに対してもX-Requested-Withヘッダーを付加することには副作用もあり、CORSによる正当なクロスサイトリクエストに対しても、プリフライト(HTTPのOPTIONSリクエスト)が発生してしまい、通信効率を低下させる要素にもなるため、注意が必要です。
Same-Origin Policyの判断基準
厳密に「アクセス先のホスト名」「プロトコル」「ポート番号」です。これについてはRFC 6454で定義されています。
しかし、Internet Explorerはポート番号が違ってもSame-Originと判定します。
また、Internet Explorerにはゾーンのセキュリティレベル設定があり、ローカルイントラネットでは、(確認ダイアログは表示されますが)クロスドメインのリクエストが可能な設定になっています。
sandbox
iframeタグのsandbox属性と、CSP (Content-Security-Policy)のsandboxパラメーターで、ページをsandbox化することができます。sandbox化されたページのオリジンは、いっさい他のページとは一致しないUnique-Originとなります。Cookieを共有したり、Web Storageにアクセスしたり、画像のピクセルを読み出すことはできなくなります。
sandbox属性にallow-same-originを指定すると、他のsandbox属性の機能を維持したまま、Same-Origin Policyに関するsandboxだけを緩和することができます。
ここでいう緩和は、そもそもsandboxを指定しなかった状態に戻すことを意味するので、sandbox属性を使ってSame-Origin Policyを回避できるわけではありません。
file URI
ところでモダンブラウザーのURIはそう簡単ではなく、ユーザーが明示的に使うことがあまりないようなURI形式もいくつか存在します。ローカルファイルの読み込みには file: で始まるfile URIが使われますが、file URIに同じポリシー(同一プロトコル、同一ホスト、同一ポート番号)が適用されると、ローカルにあるHTMLファイルは他のローカルファイルを読み込み放題ということになります。
ローカルに配置したアプリケーションのHELPドキュメントにスクリプトを仕込み、他のファイルを読み出す… そんなことが簡単にできたら、恐ろしくてローカルHTMLをブラウザーで開くことはできません。
そこで、file URIに関しては、ディレクトリーまで一致を確認することになっています。
about URI, data URI
空白のWindowを生成すると、ブラウザーによっては about:blank と表示されますが、このような(ドメイン名を含まない)ドキュメントのOriginは、これを開いた元となるOriginを継承することになっています。
その中にdata URIがあります。data URIを使うとあらゆるコンテンツをインライン化することができるので、JavaScriptからドキュメントや画像を生成するために便利ですが、JavaScript自体を生成することも可能です。
このdata URIを持つJavaScriptのOriginはどうあるべきでしょうか?元のJavaScriptから意図的に生成したJavaScriptですから、元ドキュメントと同じOriginとしても良い(その方ができることが増えて便利)とも考えられるわけです。
しかし、ここは意図的に生成したの部分を疑ってかかる、つまりXSS対策を考える必要があります。一般的なXSS対策では、ユーザからの入力に対してサニタイズ処理をすることで危険を回避するのですが、data URIではbase64 encodingが使われるため、一見安全に見える文字列から危険なJavaScriptが生成される可能性があります。
実際にはdata URIのOriginはブラウザーの実装によって統一されていない状況です。
2021年現在のWebコンテンツを考えたときに、data URIはSPA (Single Page Application)で大活躍します。さらに、2018年頃からiOS, Android, Windows 10 1803で揃ってPWA (Progressive Web Application)がサポートされたこともあり、PWAでオフライン動作、アプリ化したいときには、data URIを使いたいシーンは増えるばかりです。単にdata URIがSame-Origin Policyで制限される方向だけでは、PWAの普及に大きな足かせになる可能性があります。実際、2017年にChrome 58からdata URIでWorkerが生成できるようになるなど、この部分の動きは活発です。
Same-Origin Policyに従って制限されること
Cookie
- 厳密に言えば、CookieのSame-Origin PolicyはCookieの仕様によって決まっています。単なるオリジン一致ではなく、上位下位といった概念を含むので、ちょっと違うのですが、Cookieをセットしたり、参照したりといった行為がオリジンによって制限されているという意味では同様です。
XDR (Cross Domain Request)
- JavaScriptからXMLHttpRequestやfetch APIを使ってサーバーからコンテンツを取得すること。コンテンツの内容は問わず、原則としてSame-Origin Policyによって制限を受け、他ドメインからのデータを受け取ることはできませんが、後述するさまざまな回避方法が提供されています。
Web Storage
- key/value型のデータをブラウザーローカルに保存しますが、Same-Origin Policyのもとに隔離されています。Same-Originではないデータベースはまったく別物として扱われ、Cookieのようにpathやdomainを指定する機能はありません。
IndexedDB
- オブジェクト指向データベースとして、比較的大きなサイズのデータをWorkerからも非同期で扱うことができるデータベースです。Web Storageと同様にSame-Origin Policyで隔離され、オリジンを超える機能は用意されていません
メディア(img/video/audioタグ)
- img/video/audioタグ自体はクロスサイトの画像/動画/音声を読み込むことができます。しかし、オリジンの異なるデータとしてマーキングされた状態で読み込まれます。これらSame-Origin Policyに合致しないメディアデータは、表示や再生に利用することはできますが、生のデータを読み出すことができません。
scriptタグ
- imgタグ同様、scriptタグもクロスサイトのスクリプトを読み込むことは可能です。スクリプト機能そのものに制限はありませんが、オリジンの異なるスクリプトではエラーの詳細が報告されません。 これは、クロスサイトのリソースをあえてscriptタグで読み込み、エラー状況を解析する攻撃[XSSI]()を緩和するために設けられた機能です。
Same-Origin Policyによる制約を受けないこと
WebSocketはSame-Origin Policyによる制約を受けません。PCのIE10以降や各種モバイルブラウザーを含むモダンブラウザーは、すべてWebSocketをサポートしているので、後述するCORSを使うよりも、シンプルな実装ができる場合があります。
WebSocketがSame-Origin Policyに依存しないのは、HTTPヘッダーのカスタマイズができないことと、(CORSと同様に)Originヘッダーが出力されることにより、Same-Origin Policyレベルのセキュリティーは保たれると考えられるためです。
Same-Origin Policyを回避する
Same-Origin Policyを正しく回避し、Webの正規のルールに則って実質的にクロスサイトのリクエストを実現する方法は複数あります。いずれも重要なのは、クロスサイトのリソース(画像やデータなど)を提供する側(**Resource Provider (RP)**と呼ぶことにします)が、クロスサイトのリクエスト元となるWebページの提供者(**Service Provider (SP)**と呼ぶことにします)に対して許諾するということです。
document.domainの変更
iframeで共通のドメイン要素を持つクロスサイトコンテンツがフレームを形成する場合、双方がdocument.domainを変更することでOriginを変更し、DOM要素に直接アクセスすることができます。
<!-- 下記another.example.comページ内でもJavaScriptから
document.domain = 'example.com'
を指定する必要があります -->
<iframe src="http://another.example.com/"></iframe>
<script>
document.domain = 'example.com';
var ifr = document.querySelector('iframe');
ifr.onload = function(e) {
// ifrを通してフレーム内のDOM要素にアクセス
};
</script>
iframeタグにsandbox属性をつけると、この方法を使えないようにすることができます。
JSONP
JavaScriptからクロスドメインのJSONデータをXMLHttpRequestなどで取得しようとすると、Same-Origin Policy違反となります。一方で、scriptタグでクロスドメインのJavaScriptを読み込むことは自由にできます。
これを利用してRP側のサイトで、単純なJSONではなく、特定の関数で囲んだJSONを出力するようにして、SP側のWebページでは(XMLHttpRequestではなく)scriptタグで読み込めるようにしたのが、JSONPと呼ばれるデータ取得方法です。
例えば、RP側から渡したいJSONが
{
"name": "example",
"value": 999
}
であるとき、これを直接scriptタグで読み込んでも値が残らないため、読み取ることができません。(配列を含んでいる場合には、Arrayコンストラクターを再定義することでJSON Hijackingの攻撃が可能であることは前述した通りです)
RP側のサイトと合意がある場合、RP側でこれを
foo({
"name": "example",
"value": 999
})
のように、foo()で囲んであげることで、このJSONを引数にfoo()が呼ばれ、SP側のWebページではこのJSONを実質的に利用することができます。SP側のサイトでは、JSONが必要なときに以下のような処理をします。
function foo(obj) {
... // JSONを解釈したobjによる処理
}
var script = document.createElement("script");
script.src = "http://example.com/foo.js?callback=foo";
document.body.appendChild(script);
CORSが整備されていない時代においては、JSONPが用いられていましたが、現在ではCORSを利用した方が良い理由が数多くあります。
- JSONPはscriptタグによって読み込むため、HTTPリクエスト・ヘッダーを必要に応じてカスタマイズすることができない
- RP側サーバーは正当なSPによるWebページであることを認証する方法をパラメータやCookieなどによって用意しなければならない
- GET以外のHTTPメソッドが使えない
- JSONの解釈に安全なJSON.parse()が使えないため、RPサイトが改ざんされた場合にSPサイトに大きな被害が出る可能性がある
Cross-Origin Resource Sharing (CORS)
CORSと簡単に呼ぶのですが、HTMLタグ、HTTPヘッダー、認証、HTTPの別リクエストに及ぶ意外に広い概念を含んでいます。一義的には、クロスサイトのリクエストに対して、少なくとも以下の2点を満たすことでクロスサイトデータの授受を行う合意があると見なすためのしくみです。
- クライアント(ブラウザー)はドキュメントのOriginを表すOriginヘッダーをつける
- RPはCORSの受け入れ範囲を示すAccess-Control-Allow-Originヘッダーをつける
最小限の実装としては、RPが常にAccess-Control-Allow-Origin: *を返す(クライアントからのOriginヘッダーは気にしない)ことにより、どのサイトからのCORSでも受け入れることができます。
CORSに準拠したOriginヘッダーをリクエストヘッダーに含むためには、主に2つの方法があります。
- img/audio/video/script/linkの5つのタグにcrossorigin属性を指定する
- XMLHttpRequestまたはfetch APIを使い、RPにリクエストを発行する
認証付きリクエストと応答
CORSには認証付きと認証なしの2種類あり、これらは別物と考えた方が良いでしょう。前述した「RPが常にAccess-Control-Allow-Origin: *
を返す」実装は、認証なしの場合に限ります。
認証付きの場合、以下の要件が加えられます。
- RPはHTTP応答ヘッダーに
Access-Control-Allow-Credentials: true
を含める - RPはAccess-Control-Allow-Originにワイルドカード(*)を使えない
- 認証情報がCookieを利用する場合(多くはそうです)、CookieにSameSite=NoneとSecure属性が与えられている
- サイトの接続がhttpsである(上記CookieのSecure属性による制限)
認証付きのリクエストを発行する場合、HTMLのタグやJavaScriptのコードに、次節以降で説明する特別な記述が必要です。
crossorigin属性
crossorigin属性なしで読み込んだデータは別オリジンのデータとして明確に区別されます。画像などを表示することはできても、生データにアクセスすることはできず、canvasに描画すると、Tainted Canvasになります。
crossorigin属性にはanonymousとuse-credentialsの2種類を指定することができます。
anonymousを指定した場合、CORSのリクエストにはAuthentication, Cookieのヘッダーが含まれません。また、サーバーがHTTPSのクライアント認証を要求しても、クライアント証明書は送信されません。
use-credentialsを指定した場合、anonymousの制限は適用されず、認証情報のついたリクエストが発行されます。この場合、サーバー(RP)の応答は前節の認証付き応答に準拠している必要があり、これを満たさない場合、JavaScriptはCORSによって得られたデータを利用することはできず、空文字列の扱いになります。
XMLHttpRequest (XHR)
クロスサイトでXHRによるリクエストをする場合、原則として特別な記述は必要ありません。Same-Origin Policyに沿ったリクエストの場合とまったく同様のコードで、前節のcrossorigin属性をanonymousとする場合と同等のリクエストを発行することができます。
認証付きのリクエストを送る場合には、withCredentialsをtrueに設定します。
let xhr = new XMLHttpRequest();
xhr.open("GET", "http://example.com/foo", true);
xhr.onload = function(e) {
... // 実際の処理
};
xhr.withCredentials = true;
xhr.send(null);
今ではIE10未満(IE9以前)を気にする必要性は下がっているかもしれませんが、IE10未満の場合、XMLHttpRequestの代わりに、XDomainRequestを使う必要があります。
また、IEの場合、withCredentialsをtrueにしても、それだけではCookieヘッダーが送信されません。内容は問わないので、サーバー側でP3Pヘッダーを設定する必要があります。
fetch API
fetchの第2引数に{ mode: "cors" }
を指定します。Same-Origin Policyに含まれないURIへのfetchリクエストはデフォルトがCORSなので、省略しても問題ありません。IEは(IE11でも)fetch APIに対応していませんが、polyfillがあります。
ServiceWorkerからコンテンツを取得する場合、(Same-Origin Policyとは直接関係ない話ですが)XMLHttpRequestは使えないので、fetchを使う必要があります。
fetch("http://example.com/foo", { mode: "cors" })
.then(function(resp) {
... // 実際の処理
});
反対に"no-cors"を指定してopaque objectを取得することもできます。opaque objectの中身にアクセスすることはできませんが、内容にアクセスせずにServceWorkerに一時的に保持するような場合に有効です。
プリフライト・リクエスト
CORSを利用する場合、CORSに対応していないサーバーがいきなり重大な(残念な)結果を引き起こしてしまわないようにプリフライト・リクエストというしくみがあります。
リソースの取得という側面だけで考えると、CORSのリクエストが失敗しても、ブラウザー(SP側のWebページ)からは取得できないだけなので問題はありません。しかし、CORSのリクエストが何らかのコンテンツ変更を引き起こすものである場合、サーバーが正しく実装されていなければ、問題のあるリクエストによって大切なデータが削除されてしまうかもしれません。
こうした問題を回避するために、CORSでは
- ほぼリクエストヘッダーのカスタマイズなし(正確な定義は仕様を参照)
- GET/POST/HEADのいずれかのメソッド
- POSTの場合、送信するのがフォームまたはプレーンテキストである
の条件を満たす以外の場合、本来のリクエストに先立ってプリフライト・リクエストを送信します。
プリフライト・リクエストはHTTPのOPTIONSメソッドで、CORSの動作を制御するために以下のHTTPヘッダーがあります。プリフライト・リクエストはブラウザーが自動的に作成して送信するため、JavaScriptコードが明示的に意識する必要はありません。
しかし、RPサーバー側では、これらの内容を十分に理解してOPTIONSリクエストに対する応答を設定しておく必要があります。
送受信種別 | ヘッダー文字列 | 意味 |
---|---|---|
リクエスト | Access-Control-Request-Method | リクエスト本体が利用するHTTPメソッドを指定する |
リクエスト | Access-Control-Request-Headers | リクエスト本体が利用するHTTPヘッダーを指定する |
応答 | Access-Control-Expose-Headers | CORSにおけるサーバーの応答をSP側のWebページ内のJavaScriptで読み取る際に読み取って良いヘッダーを指定する |
応答 | Access-Control-Max-Age | プリフライト・リクエストの有効期間を指定する |
応答 | Access-Control-Allow-Credentials | trueの値を指定することで認証付きリクエストが成功したことを示す |
応答 | Access-Control-Allow-Methods | リクエスト本体で利用することを許可するHTTPメソッドを指定する |
応答 | Access-Control-Allow-Headers | リクエスト本体で利用することを許可するHTTPヘッダーを指定する |
Cross Document Messaging
Webページが他のWebページのJavaScriptによってポップアップ表示された場合、またはiframeで内部に表示された場合、それらコンテンツには親子関係があります。この場合の親子関係はSame-Originでなくても成立します。(CSPによって制限される場合もあります)
親子関係があるからといって、それぞれのコンテンツ内部にアクセスすることはできませんが、親子に限った特別なコミュニケーション手段としてCross Document Messagingが用意されています。
Cross Domain Messagingを受ける側(子Webページ)では、messageイベントを実装して、親からのメッセージを受け取ります。messageイベントを実装しない限り、セキュリティー上の問題は発生しないため、Cross Domain MessagingはSame-Origin Policyには制限されません。
Channel Messaging API
Webページに親子関係がない場合でも、複数のWebページがフレームやSharedWorkerを通してコミュニケーションをしたい場合があります。
そのような場合に、モダンブラウザーでは、Cross Domain Messagingと似たしくみであるChannel Messaging APIを使うことができます。
Resource Timing API
IE10を含むモダンブラウザーでは、ページ内のリソース取得時間をミリ秒単位でモニターするためのResource Timing APIが利用できます。読み込みが遅いリソースを検出し、Webページの表示を最適化することが主な目的と考えられます。
Resource Timingはクロスドメインのリソースについてもタイミング情報を取得することができます。本来の目的から考えると、読み込みが遅いサーバーを検出してキャパシティー配分を見直したり、CDNの利用を検討したり、ネットワーク構成を見直したりという使い方が想定され得るため、むしろクロスドメインでタイミングを計測できなければ意味がありません。
しかし、あらゆるクロスドメインリソースの読み込みタイミングを計測できてしまうと、SameSite Cookieの節で書いた通り、内容を見ることができないクロスドメインコンテンツの中身を推測できてしまう場合があります。
このため、Resource Timing APIでは、クロスドメインのコンテンツ取得時に高精度な取得時間をモニターできるかどうかを、RP側のサーバーで指定できるTiming-Allow-Originヘッダーを規定しています。Timing-Allow-OriginヘッダーにはAccess-Control-Allow-Originヘッダーと同じ内容を設定することで、SP側のWebページのJavaScriptは、RP側リソースの読み込みタイミング情報を取得することができます。
Tainted Canvas - Canvasの汚染
CanvasはDOMから見るとほぼimgタグ同等の画像要素です。JavaScriptからは自由に読み書きをすることができ、画像のダイナミックな生成加工や、2D/3Dのゲーム画面などに使われます。
画像を加工する際に、元となる画像を読み込むことは一般的です。
var canvas = document.createElement("canvas");
document.body.appendChild(canvas);
var context = canvas.getContext("2d");
var image = new Image();
image.onload = function() {
canvas.setAttribute("width", image.width);
canvas.setAttribute("height", image.height);
context.drawImage(image, 0, 0);
};
image.crossOrigin = "anonymous"; // CORSの設定
image.src = "http://example.com/images/foo.png";
このとき、CORSが正しく処理されていない場合には、画像が読み込まれて表示されても、画像の中身を利用することができないTainted Canvasの状態になります。
汚染された画像は、正しくcrossorigin属性をつけないimgタグやImageオブジェクト、videoタグによって発生します。それだけではなく、contenteditableなdivに他サイトの画像を(ユーザー操作やpasteイベントによって)貼り付けた場合にも、汚染された画像が含まれる場合があります。
本題から少しそれますが、Firefox 52 / Chrome 66からImageBitmapによってBlobから直接canvasに描画できるようになりました。これまでは1度Imageを作ってからコピーしていたので、速度面、メモリ効率面でメリットが大きく、コードもシンプルになります。
最新ブラウザー限定なので、以下のサンプルコードはES6スタイルで書いています。
const canvas = document.createElement("canvas");
document.body.appendChild(canvas);
canvas.getContext("bitmaprenderer").transferFromImageBitmap(
await createImageBitmap(
await (await fetch("http://example.com/images/foo.png")).blob())));
Public Suffix
直接Same-Originとは関係ないのですが、モダンブラウザーはSupercookieを防止するためにPublic Suffix Listを利用しています。
このため、自社のドメインであれば、配下の複数サーバーで共有するCookieを発行することができるのに対して、例えばco.jp全体に有効なCookieを発行することはできません。
domain cookieや、document.domainの書き換えの際に、こうしたListが使われていることを頭の片隅に置いておくと、トラブル防止に役立つかもしれません。
最後に
PWA (Progressive Web Apps)の出現によって、Webはブラウザーで見るコンテンツからアプリケーションへと、その存在意義を広げつつあります。裏を返せば、攻撃者が利用できる要素も飛躍的に増え、Web開発者にとって正しいセキュリティー知識の必要性もいっそう高まっています。
Same-Origin Policyを軸としたWebセキュリティーの理解を深め、より安全で楽しいWebアプリケーションが増えてくれるといいな、と思っています。
-
あくまで技術的な意味であり、著作者の意思によって制限される場合があります。技術的に可能だからといって、それが利用契約上または社会通念上許されるかどうかとは別問題です。 ↩