平成31年度 春期 情報処理安全確保支援士試験 午後Ⅰ 問1|便利なWeb API連携が情報漏えいの穴になる——情報処理安全確保支援士で学ぶ同一オリジンポリシ・JSONP・CORSの事例
この記事で扱うセキュリティ事例
- 試験区分: 情報処理安全確保支援士
- 出典: 平成31年度 春期 情報処理安全確保支援士試験 午後1 問1
- 主なテーマ: Webサイトのセキュリティ、Web API、同一オリジンポリシ、JSONP、CORS、Cookieとwithcredentials
この記事は「情報処理安全確保支援士 午後問題 全70問」シリーズの1本です。→ 全70問の一覧・テーマ別索引はこちら
導入: なぜこの事例が重要なのか
「複数のサイトのデータをまとめて見せたい」——Webサービスを運営していれば、誰もが一度は考える便利な機能です。ところが、その「サイトをまたいでデータをやり取りする」という発想は、Webブラウザがもっとも厳しく見張っているポイントに、まっすぐ突き当たります。情報処理安全確保支援士の午後1で出題されたこの事例は、小売業の会社が自社の複数サイトを連携させようとしたとき、開発者が良かれと思って選んだ回避策が、会員10万名の個人情報を抜き取られる穴になりかけた物語です。
この記事のテーマは、同一オリジンポリシ(ブラウザがサイトをまたいだアクセスを制限する基本ルール)、JSONP(その制限をすり抜ける古いテクニック)、そしてCORS(サイトをまたいだアクセスを安全に許可する正しい仕組み)です。どれもWebアプリケーションのセキュリティの根幹にありながら、言葉だけ聞くと「なんとなく難しそう」で終わりがちな領域です。
この記事を読むと、なぜブラウザはサイトをまたいだアクセスを止めるのか、JSONPの何が危険なのか、CORSがどんな順番で「これは許可していい通信か」を確認しているのか、そしてCookieという資格情報がどのタイミングで送られるのかが、専門外の方にも見えてきます。受験者には頻出のWebセキュリティ論点が一通り詰まっており、サービスを企画・発注する立場の方には「便利機能の裏に潜むWebの落とし穴」と「リリース前に脆弱性診断を挟む価値」が伝わる教材になっています。
インシデントの概要

舞台は、従業員200名の小売業、M社です。M社は、会社全体の顔となるコーポレートサイト「WebサイトA」(URL: https://site-a.m-sha.co.jp/ )を持ち、それとは別に、自社の特定ブランドを扱うECサイト(ネット通販サイト)を複数運営していました。ブランドサイトはWebサイトBからFまでの五つあり、これらの開発も運用も、すべて自社の開発部が担っていました。
WebサイトAは、各ブランドのキャンペーン情報を集めて掲載する「入り口」のようなポータルサイトで、会員専用の機能はありません。一方、ブランドBの商品を扱うECサイトであるWebサイトB(URL: https://site-b.m-sha.co.jp/ )には会員数10万名がいて、Cookie(ブラウザに保存される小さな識別情報)を使ったセッション管理——つまり「いまログインしているのが誰か」を覚えておく仕組み——で運営されていました。会員情報は、ブランドサイトごとに個別に管理されています。
M社は、ここで一つの便利な企画を立てます。各ブランドサイトの売上をもとにした「ブランド別の売れ筋商品情報」を、ポータルであるWebサイトAにまとめて表示し、さらに希望する会員には電子メールでも定期配信しよう、というものです。この企画を実現するには、各ブランドサイトが持っている「売れ筋商品情報」と「会員情報」を、WebサイトAが取得できるようにしなければなりません。これが「情報連携機能」です。
連携には二つの機能が必要でした。機能1は、WebサイトAが各ブランドサイトの売れ筋商品情報を取得すること。機能2は、メール配信を希望する会員の会員情報を、WebサイトAが取得することです。配信の申込みそのものは、ポータルであるWebサイトA上で行います。この機能を、まずはWebサイトBとの連携から実装しようと、開発部のCさんが手を動かし始めたところから、この物語は動き出します。
何が起きたのか

Cさんはまず、WebサイトBに二つのWeb API(外部のプログラムから決まった形式でデータを呼び出せる窓口)を用意しました。一つはAPI-X——WebサイトBの売れ筋商品情報を取得できるAPI。もう一つはAPI-Y——WebサイトBの会員情報を取得できるAPIです。受け渡すデータの形式には、JSON(JavaScript Object Notation。プログラムが扱いやすい、軽量なデータ記述形式)を採用しました。
売れ筋商品情報を扱うAPI-Xのほうは、比較的素直に設計できました。これはWebサイトAというサーバが直接WebサイトBのサーバに取りに行く通信なので、「API-XにはWebサイトAからだけアクセスできるよう、接続元のIPアドレスを制限する」という守りをかけられます。決まった相手からしか呼ばれないAPIなら、相手のIPアドレスで入口を絞れるわけです。
問題は、会員情報を扱うAPI-Yのほうでした。Cさんは、会員情報を取得するなら、配信を希望した会員本人の同意のもとで取りたいと考えます。そこで、会員情報の取得を「サーバ同士」ではなく「会員のWebブラウザを経由して」行う方式を選びました。会員がWebサイトAの申込ページにアクセスすると、申込ページと一緒に、WebサイトBのAPI-Yを呼び出す小さなプログラム「スクリプトZ」がブラウザに返されます。会員が申込みを行うと、このスクリプトZがブラウザの中から会員情報の取得リクエストをAPI-Yへ送り、返ってきた会員情報をブラウザに表示したうえで、申込情報とともにWebサイトAへ保存する——そういう流れです。会員がWebサイトBにログインした状態のときにアクセスすることで、ログイン中の本人の情報だけが取れる、という前提でした。
ところが、この方式を実装しようとしたCさんは、壁にぶつかります。スクリプトZは、WebサイトA( https://site-a... )の申込ページから読み込まれて動いているのに、データの取得先はWebサイトB( https://site-b... )です。これは、ブラウザから見れば「別のサイトへのアクセス」になります。そして、ここにこの事例の核心である【同一オリジンポリシ】が立ちはだかるのです。
同一オリジンポリシとは、ブラウザに組み込まれた安全装置で、あるサイトから読み込まれたスクリプトが、別の「オリジン」のリソースへ自由にアクセスすることを制限する仕組みです。ここでいうオリジンとは、URLのうち「スキーム(httpかhttpsか)」「FQDN(ホスト名。site-a.m-sha.co.jp のような完全なドメイン名)」「ポート番号(通信の出入口を表す番号)」の三つの組み合わせを指します。この三つのうち一つでも違えば、ブラウザは「別のサイト=クロスオリジン」と判断し、勝手なデータの読み取りをブロックします。WebサイトAとWebサイトBは、FQDNが site-a と site-b で異なります。だから、スクリプトZがブラウザの中からAPI-Yを普通に呼んでも、ブラウザがその応答の読み取りを止めてしまうのです。
なぜブラウザがこんなに厳しいかというと、もしこの制限がなければ、悪意あるサイトを開いただけで、そのサイトのスクリプトがあなたのログイン済みの銀行サイトやSNSに勝手にアクセスし、データを読み取れてしまうからです。同一オリジンポリシは、Webの利用者を守る最後の砦のような存在です。
そこでCさんは、この制限を回避する手段として、【JSONP】(JavaScript Object Notation with Padding)を使うことを思いつき、上司のD課長に提案します。JSONPは、同一オリジンポリシの「例外」をうまく突いた古典的なテクニックです。ブラウザは、`<script>`タグで外部のJavaScriptを読み込むことについては、別のサイトからでも許しています。JSONPはこの性質を利用して、データを「スクリプトの形」で読み込ませることで、サイトをまたいだデータ取得を実現します。たしかに、これを使えば制限は回避できます。
しかし、提案を聞いたD課長は、はっきりと待ったをかけました。「JSONPには、アクセス先を制限する機能がない。だから危険だ」と。
D課長が指摘した危険のシナリオは、こうです。攻撃者が、会員情報を盗み出すように、このスクリプトZを書き換える。そして書き換えたスクリプトを、攻撃者自身が用意したWebサイトの罠ページに仕込んでおく。そのうえで被害者を誘導し、ある「特定の操作」をさせたうえで、その罠ページにアクセスさせます。すると——JSONPには送信先を絞る仕組みがないため——被害者のブラウザは、罠ページから来た指示に従って、WebサイトBのAPI-Yへ会員情報の取得リクエストを送ってしまう。しかもそのとき、被害者がWebサイトBにログイン済みであれば、ブラウザはWebサイトBのCookie(ログイン状態を示す資格情報)を自動的に付けて送ります。API-Yから見れば、それは正規のログイン会員からの正当なリクエストにしか見えません。こうして攻撃者は、罠ページを通じて被害者本人の会員情報を、まんまと窃取できてしまうのです。
ここで鍵になるのが、D課長の言う「特定の操作」が何かという点です。この攻撃が成立するには、被害者がWebサイトBにログインした状態であることが絶対条件でした。ログインしていなければCookieは付かず、API-Yは会員情報を返さずエラーになります。つまり攻撃者が被害者にさせたい「特定の操作」とは、【WebサイトBへのログイン】にほかなりません。便利だと思って選んだJSONPが、「ログイン中のユーザーを罠にかけて情報を抜く」という、まさに同一オリジンポリシが防ごうとしていた攻撃への扉を開けてしまうところだったのです。
ではどうすればいいのか。Cさんの問いに、D課長はもう一つの技術を示します。【CORS】(Cross-Origin Resource Sharing)です。CORSは、あるサイトから別のサイトへのアクセスを「制御できる」仕組みです。JSONPと違って、アクセス元を見て許可・不許可を判断できる点が決定的に違います。
CORSの動きは、二段構えになっています。まずブラウザは、本番のデータ要求(メインリクエスト)をいきなり送る前に、「プリフライトリクエスト」という事前確認の通信を送ります。これはOPTIONSというメソッド(HTTPの通信種別の一つ)で送られ、その中に「これからどんなメソッドやヘッダで本番のリクエストを送りたいか」と、自分がどのサイトかを示す「Originヘッダ」(例: https://site-a.m-sha.co.jp )を入れておきます。
これを受け取ったサーバ側のAPIは、「あなたのオリジンからのアクセスを許可しますよ」という意思を、応答の「Access-Control-Allow-Origin」というヘッダに、許可する相手のオリジンを書いて返します。あわせて、許可するメソッドやヘッダの一覧も返します。ブラウザは、この返ってきた許可情報を見て、「自分がスクリプトを読み込んだページのオリジン」と「サーバが許可したオリジン」を照合します。一致して許可が確認できて初めて、ブラウザは本番のメインリクエストを送ります。許可が確認できなければ、メインリクエストは送らずにそこで処理を打ち切ります。
この事例の実装でいえば、流れはこうなります。会員がWebサイトAの申込ページにアクセスすると、WebサイトAはAPI-Yを呼び出すスクリプトZを含むページを返します。会員が申込みを行うと、ブラウザはWebサイトBへプリフライトリクエストを送ります。このリクエストのOriginヘッダには、申込ページのオリジンである "https://site-a.m-sha.co.jp" が入っています。API-Yは、リクエストにOriginヘッダがある場合、Access-Control-Allow-Originヘッダを付けて応答を返します。その値は、許可するオリジン——つまり申込ページのオリジンである "https://site-a.m-sha.co.jp" です。Originヘッダがなければ、API-Yはエラーを返します。ブラウザは、自分が読み込んだ申込ページのオリジンと、返ってきたAccess-Control-Allow-Originの値を照合し、許可されていることを確認できて初めて、本番の会員情報取得リクエストを送るのです。
このとき、Cookieのような資格情報がどう扱われるかも重要なポイントでした。CORSでは通常、ブラウザはスクリプトを読み込んだページのオリジンに対してだけCookieやベーシック認証情報を送ります。ただし、XMLHttpRequest(ブラウザがサーバと裏側で通信するための仕組み)の「withCredentials」というプロパティをtrueに設定しておくと、クロスオリジンの相手に対しても資格情報を付けて送れるようになります。そしてこのCookieが実際に送られるのは、事前確認であるプリフライトリクエストの段階ではなく、本番のメインリクエストの段階です。プリフライトはあくまで「許可を確認するための下見」なので、そこに資格情報は乗せないのです。
CORSであれば、API-Y側が「許可するのはWebサイトAだけ」と明示的に決められます。攻撃者の罠ページのオリジンは許可リストに入っていないので、たとえ被害者がログイン済みでも、ブラウザがメインリクエストを送る前に止めてくれる。JSONPでは閉じられなかった穴が、CORSでは正しく閉じられるのです。
物語の締めくくりとして、D課長はもう一歩先を見ていました。「今後、他のシステムでもCORSを使うだろうから、コーディング規約もまとめておきたい」と。というのも、Access-Control-Allow-Originに指定できるオリジンは一つだけです。今回はWebサイトAだけを許可すれば足りましたが、将来「複数のオリジンからのアクセスを許可したい」となったとき、一つしか書けない仕様では足りません。そこでD課長は、Web APIのプログラムの中に「許可するオリジンのリスト」をあらかじめ用意しておき、リクエストが来たらその中のOriginヘッダの値をリストと突き合わせ、一致するものがあればその値をAccess-Control-Allow-Originに設定して返す——という方針を、規約に含めることを提案しました。こうすれば、複数オリジンに対応しつつ、許可していない相手は確実にはじけます。
その後、M社は情報連携機能のリリース前に脆弱性診断を依頼し、脆弱性が検出されないことを確認したうえで機能を公開しました。さらに、残る四つのブランドサイトからの情報連携機能も同じ方式で実装していきました。良かれと思った回避策の危うさを上司が見抜き、正しい仕組みへ導いたことで、10万名の会員情報を守るリリースにたどり着いたのです。
ここから先は

情報処理安全確保支援士(登録セキスペ)の午後問題、「読んでも頭に入らない」と感じていませんか?本シリーズでは、2017年〜2025年の過去…
この記事が気に入ったらチップで応援してみませんか?
