情報処理安全確保支援士 令和5年秋 午後問1解説 ── 16件のレビューが2件しか表示されない格納型XSS

· · 情報処理安全確保支援士, 登録セキスペ, XSS, クロスサイトスクリプティング, Webアプリケーション, 情報セキュリティ, 脆弱性, IPA, セッション管理

「16件のレビューがあるはずなのに、2件しか表示されない」

情報処理安全確保支援士試験 令和5年度 秋期 午後 問1は、このユーザーからの問合せから始まります1。画面遷移におかしなところはなく、エラーも出ていません。ただ、表示件数が合わない。

原因は格納型のクロスサイトスクリプティング(XSS)でした。しかしこの問題が面白いのは、XSSという答えそのものではありません。Q社(問題文に登場するEC事業者)がすでに持っていた「それらしい対策」が、ことごとくすり抜けられているところです。レビュータイトルの50字制限は破られ、アップロードに必要なトークンは正規の手順で入手され、盗んだセッションIDは外部サーバーへ送られることなく持ち出されました。

この記事では、この大問を1問ずつ追いながら、効かなかった対策と、効いたはずの対策がどこで分かれるのかを整理します。試験対策としてだけでなく、自分たちのWebアプリケーションのコードレビュー観点としてそのまま使える内容です。

なお、Webサイト全体のセキュリティを何を基準に確認すればよいかという俯瞰の話は、IPA「安全なウェブサイトの作り方」をチェックリストとして使うで扱っています。この記事はそこで挙げた11の脆弱性のうち、XSSの1つを実例で深掘りするものです。

1.まず結論

  • 脆弱性の種類は格納型XSS。攻撃者の文字列がサーバーに保存され、以後ページを開いた人全員のHTMLに出力されていた。埋め込まれたスクリプトがDOMのAPIを使うことと、DOM Based XSSであることは別の話
  • 入力文字数制限はXSS対策にならなかった。攻撃者は投稿を15回に分割し、投稿の間に挟まるHTMLをJavaScriptのコメントで読み飛ばさせて、1本のスクリプトにつないだ
  • アップロード用のトークン(CSRF対策)もXSS対策にならなかった。攻撃スクリプトは正規の画面と同じ手順でトークンを取得している。同一オリジンで動く以上、正規利用者にできることはすべてできる
  • 盗んだセッションIDは外部へ送られていない。cookieの中身を「a.png」という画像ファイルとしてサイト自身のアイコンアップロード機能に置き、攻撃者はそれを普通に閲覧して回収した。出口対策では検知できない
  • 問題文がQ社に欠けていたと明示するのは3つ。出力時のエスケープ(根本的解決)と、cookieのHttpOnly属性アップロードファイルの形式確認(この2つは保険的対策)。どれか1つでもあれば、この攻撃の連鎖は途切れていた。ただし形式確認は、手口を変えられると通ってしまうため再エンコードまで必要になる
  • 問題文には出てこないが、CSPの script-src も同じ連鎖を断ち切れる。'unsafe-inline' を許可しない構成なら、埋め込まれたインラインスクリプトはそもそも実行されない

2.題材について ── 出典と、この記事での扱い

取り上げるのは次の問題です。

出典:令和5年度 秋期 情報処理安全確保支援士試験 午後 問1

IPAは、公表している過去の試験問題について、法令に特別の定めがある場合を除き許諾や使用料は必要ないとしています。ただし著作権を放棄しているわけではなく、出典を「年度、期、試験区分、時間区分、問番号等」の形式で明記すること、問題の一部を改変している場合はその旨も明記することを求めています2

この記事では、問題冊子に掲載されたHTMLやスクリプトをそのまま転載していません。仕組みの説明に必要な範囲で、当社が書き起こした等価なサンプルコードに置き換えています。設問文と解答例も要約して扱います。問題冊子・解答例・採点講評の原文はIPAのページから無償でダウンロードできますので、手元に開きながら読むことをおすすめします1 3 4

問題の舞台

登場するのは、洋服のEC事業を手掛ける従業員100名のQ社です。自社開発の「WebアプリQ」でECサイトを運営しており、利用者はHTTPSでアクセスします。今回、会員による商品レビュー機能を追加した、という設定です。

押さえておくべき仕様は次の5点です。

機能 仕様
ログイン 会員IDとパスワードで認証し、セッションIDをcookieとして払い出す
商品レビュー ログイン済み会員だけが投稿できる。レビュータイトルは50字、レビュー詳細は300字の入力文字数制限があり、いずれも自由記述
会員プロフィール アイコン画像をアップロードするページと、クレジットカード情報を登録するページを提供。どちらもログイン済み会員だけが利用できる
アイコン画像のアップロード /user/upload に対して、画像ファイルとトークンをパラメータとして送信する。トークンが /user/profile で払い出されたものと一致したときだけ成功する
アイコン画像の表示 アップロードされたアイコン画像は、会員プロフィール設定ページやレビューページに表示される

最後の2行が、後で効いてきます。

3.症状 ── なぜ16件が2件になったのか

会員から「無地Tシャツのレビューページ(ページV)に16件表示されるはずのレビューが2件しか表示されない」という問合せが入ります。開発部のNさんがページVを開くと、画面上は次のように見えました。

  • ヘッダーには「16件のレビュー」
  • 会員Aのレビューが1件(タイトル「Good」、本文「Nice shirt!」)
  • 会員Bのレビューが1件
  • 末尾に「以上、全16件のレビュー」

件数の表示は16件、実際に並んでいるのは2件。ここでHTMLを確認すると、会員Aによる投稿が15件存在し、そこに長いスクリプトが埋め込まれていることが分かります。

つまり、レビュー自体は16件ぶんHTMLに出力されているのです。にもかかわらず2件しか見えないのは、その大部分が <script> 要素の中身になってしまい、ブラウザが「表示すべきコンテンツ」として扱わなくなったからです。

飲み込まれる範囲は、正確には「15件まるごと」ではありません。開始タグ <script> は1件目のレビュータイトルの途中(「Good」の直後)に現れ、終了タグ </script> は15件目のレビュータイトルの末尾にあります。したがって、

  • 1件目のカード(アイコン・表示名・日付・★・タイトルの「Good」まで)は <script> より前にあるので表示される
  • そこから15件目のタイトル末尾までが script 要素の中身として飲み込まれる
  • 15件目の残り(本文の「Nice shirt!」)は </script> より後にあるので表示される

結果として、1件目のヘッダー部分と15件目の本文がつながり、画面上は「タイトルがGoodで本文がNice shirt!の、会員Aによる1件のレビュー」に見えます。これに会員Bの1件を足して、2件です。攻撃者が1件目の冒頭に「Good」、15件目の本文に「Nice shirt!」という自然な文字列を置いているのは、表示崩れを不自然に見せないためでしょう。

この「件数は合っているのに表示が合わない」という症状の読み方は、実務でも役に立ちます。表示件数のロジックにバグがあるのではなく、出力されたHTMLの構造が壊されている可能性を疑う入口になるからです。

4.設問1 ── なぜ「格納型」なのか

設問1では、この攻撃で使われたXSS脆弱性の種類を、DOM Based XSS・格納型XSS・反射型XSSの3択から選ばせます。正解は格納型XSSです。

そして、この設問についてIPAの採点講評はこう書いています。

正答率は平均的であったが、スクリプトで DOM を使用していたことからか、”DOM Based XSS” と誤って解答する受験者が散見された。

問題文に出てくる攻撃スクリプトは、XMLHttpRequest を使い、レスポンスをDOMとして受け取り、getElementById で要素を引いています。たしかにDOMを触っています。しかし、それは脆弱性の種類とは関係ありません

分かれ目は「攻撃文字列がどこでスクリプトになるか」

3類型を分けるのは、攻撃文字列がどこで実行可能なコードに変わるか、つまり脆弱な出力箇所(シンク)がサーバー側にあるかブラウザ側にあるかです。

種類 シンク(攻撃文字列が実行可能になる場所) 攻撃文字列はどこから来るか
反射型XSS サーバーがHTMLを組み立てる処理 攻撃者が用意したURLのパラメータなど、そのリクエスト自身
格納型XSS サーバーがHTMLを組み立てる処理 サーバー側に保存されたデータ
DOM Based XSS ブラウザ上のJavaScript(innerHTML への代入、evaldocument.write など) URLのフラグメント、postMessage、サーバーから受け取った値など

この問題では、攻撃者が投稿したレビュータイトルをサーバーがそのままHTMLに埋め込んで返していました。シンクはサーバー側の出力処理にあり、その文字列はデータベースに保存されて以後ページVを開いた全員に配られます。したがって格納型です。

ブラウザ上のJSがinnerHTMLなどに渡したときサーバーが組み立てたHTMLに入っていた保存されている(以後、全員に出力)保存されていない(そのリクエストにだけ)攻撃者の文字列がスクリプトとして実行された実行可能になったのはどこかDOM Based XSSその文字列はサーバーに保存されているか格納型XSS反射型XSS

なお、「サーバーのレスポンスに攻撃文字列が入っていれば格納型/反射型」と単純化するのは危険です。サーバーが値を無害なテキストやJSONとして返していても、それをブラウザ側のJavaScriptが innerHTML に代入すればDOM Based XSSになります(保存された値が原因になる、いわゆるstored DOM XSSです)。この場合に直すべきはサーバーの出力処理ではなくクライアント側のシンクなので、レスポンスに文字列が見えるかどうかではなく、どこで実行可能になったかで判定してください

埋め込まれたスクリプトが何をするか(DOMを触る、通信する、cookieを読む)と、そのスクリプトがどうやってページに入り込んだかは、別々に考える必要があります。混同しやすいのは、対策を選ぶときに困るからではなく、対策を間違えるからです。DOM Based XSSと判断すれば「クライアント側のJavaScriptを直せばよい」という結論になりますが、実際に直すべきはサーバー側の出力処理でした。

設問1(2):対策

設問1(2)はWebアプリQにおける対策を30字以内で答えさせます。解答例は「レビュータイトルを出力する前にエスケープ処理を施す」です。

ここで「出力する前に」という順序が明示されている点は重要です。入力を受け取ったときにエスケープするのではなく、HTMLとして出力する直前に、出力先の文脈に応じてエスケープします。IPA「安全なウェブサイトの作り方」でも、XSSの根本的解決の筆頭に「ウェブページに出力する全ての要素に対して、エスケープ処理を施す」が挙げられています5

「SQLインジェクションでは入力のエスケープが大事では?」

ここで必ず出てくる疑問です。結論から言うと、SQLインジェクションでも「入力時のエスケープ」は対策ではありません

IPAがSQLインジェクションの根本的解決として挙げているのは「SQL文の組み立ては全てプレースホルダで実装する」です。文字列連結でSQL文を組み立てざるを得ない場合の代替としてエスケープ処理が挙げられていますが、それも「SQL文の組み立てを文字列連結により行う場合は、エスケープ処理等を行うデータベースエンジンのAPIを用いて、SQL文のリテラルを正しく構成する」と書かれており、エスケープするのはSQL文を構成する瞬間です6。入力を受け取った時点ではありません。

つまりHTMLの場合とまったく同じ構造です。共通する原則はこうなります。

エスケープは、データがどの文法の世界へ出ていくかが決まる場所で行う。

  • HTMLとして出るなら、HTMLのエスケープ
  • SQL文の一部になるなら、プレースホルダ(やむを得なければSQLのリテラルとしてのエスケープ)
  • シェルコマンドの一部になるなら、シェルの流儀での処理

なぜ入力時ではいけないのか。入力を受け取った時点では、そのデータが将来どこへ出ていくか決まっていないからです。同じレビュー本文が、HTMLページにも、CSVのエクスポートにも、JSONのAPIレスポンスにも、通知メールにも、ログにも出ていきます。入力時にHTMLエスケープをかけてしまうと、CSVには &amp; という文字列がそのまま出てきますし、データベースの中身は元の入力と違うものになり、検索も集計も狂います。二重エスケープの温床にもなります。

では入力側は何もしなくてよいのか

いいえ。ただし入力側でやるのはエスケープではなく検証(バリデーション)です。役割が違います。

  • 入力で「弾く」 ── 仕様上ありえない値を受け付けない。郵便番号欄なら数字7桁以外を拒否する、数量欄なら負数を拒否する。これはデータの正しさを守るための、独立して必要な処理
  • 出力で「エスケープする」 ── 受け入れたデータを、出ていく先の文法で安全に表現する。これが脆弱性の根本的解決

そして、入力側の検証にXSS対策を期待しすぎないことも重要です。IPAはXSSについて、入力値がアプリケーションの仕様に沿うかを確認する方法に触れたうえで、この対策の有効性は限定的であり、アプリケーションの要求する仕様が幅広い文字種の入力を許す場合には対策にならないため、この方法に頼ることは推奨されないと明記しています5

今回のレビュータイトルとレビュー詳細は、まさに自由記述で幅広い文字種を許す仕様でした。50字や300字という上限はありますが、スクリプトを動かすのに必要な文字数はそれよりずっと少ないので、上限があること自体は防御になりません。実際にQ社で何が起きたかは、この後の設問2で見ていきます。

5.設問2 ── 50字制限はどう破られたか

設問2は、この問題の白眉です。

図3について、入力文字数制限を超える長さのスクリプトが実行されるようにした方法を、50字以内で答えよ。

解答例は「HTMLがコメントアウトされ一つのスクリプトになるような投稿を複数回に分けて行った」です。

攻撃者は、1件の投稿に収まる長さに分割して15回投稿しました。ポイントは、投稿と投稿の間に必ず挟まってしまうHTML(</div><div class="..."> など)をどう処理するかです。これらはJavaScriptとしては構文エラーになります。

そこで使われたのが、JavaScriptのブロックコメントです。次は、仕組みを説明するために当社で書き起こした簡略版です(問題冊子の図をそのまま引用したものではありません)。

3回に分けて、レビュータイトル欄にこう投稿したとします。

1件目: すばらしい<script>a=1;/*
2件目: */b=2;/*
3件目: */c=3;</script>

サーバーは、これをそれぞれのレビューのタイトルとして、以下のようにHTMLへ出力します。

<div class="review-title">すばらしい<script>a=1;/*</div>
<div class="description">…</div>
<div class="review-title">*/b=2;/*</div>
<div class="description">…</div>
<div class="review-title">*/c=3;</script></div>

ブラウザから見ると、最初の <script> から最後の </script> までが1つのscript要素です。その中身は次のようになります。

a=1;/*</div>
<div class="description">…</div>
<div class="review-title">*/b=2;/*</div>
<div class="description">…</div>
<div class="review-title">*/c=3;

/**/ に挟まれたHTMLはJavaScriptのコメントとして読み飛ばされ、実際に実行されるのは a=1; b=2; c=3; だけになります。そして、コメントとして読み飛ばされた部分も、script要素の中身である以上、画面には表示されません。これが「16件が2件に見える」症状の正体です。

ここから持ち帰るべきこと

ここで因果を取り違えないでください。入力文字数制限がXSS対策にならないのは、投稿回数が無制限だったからではありません。そもそも50字あればスクリプトは実行できるからです。イベントハンドラ属性を1つ書き足すだけ、あるいは外部のスクリプトを読み込むタグを1つ置くだけなら、いずれも数十文字で収まります。投稿を1件に制限したとしても、文字数制限が防御になることはありません。

では今回の分割投稿は何だったのか。あれは20行ほどある長いスクリプトを丸ごと持ち込むための手段です。攻撃者がやりたいことが長かったので回数で稼いだ、という話であって、文字数制限が破られた理由そのものではありません。手口の説明と、対策としての評価は分けて考える必要があります。

同じことは、他の「入力側の制限」全般に言えます。

  • 文字数制限、入力可能文字種の制限、フロントエンドのバリデーション ── これらは仕様上必要なものであって、XSSの根本的解決の代わりにはならない
  • 攻撃者は分割・エンコード・別経路からの投入といった手段で、入力側の制限を回避する余地を常に持っている
  • 守るべき場所は、データがHTMLとして出ていく瞬間である

なお、IPAの採点講評はこの設問について「”開発者ツールで入力制限を削除してから投稿した”のように、確認が不足していると考えられる解答が一部に見られた」とも書いています。たしかにフロントエンドの制限は開発者ツールで外せます。しかしこの設問で求められていたのは、HTMLに残された痕跡から実際に何が行われたかを読み取ることです。図3のHTMLには、制限内に収まる長さの投稿が15件に分かれて並び、それぞれの先頭と末尾にコメント記号が置かれていました。これは制限を「外した」痕跡ではなく、制限を「回避した」痕跡です。攻撃者の残したものを一つずつ確認する姿勢が問われていた、と読むべきでしょう。

入力値をどこまでどう検証するかという設計の考え方は、QRコードの読取値をアプリでどう検証するかでも、外部から入ってくるデータを信用しない設計として整理しています。

6.設問3(1) ── スクリプトは何をしたか

Nさんが抽出したスクリプトは20行ほどです。設問3(1)は、そのうち6行目から20行目(1回目の通信が成功してから実行される部分)の処理内容を60字以内で答えさせます。

解答例は「XHRのレスポンスから取得したトークンとともに、アイコン画像としてセッションIDをアップロードする」です。

以下は、この動作を説明するために当社で書き起こした等価なコードです(問題冊子の図をそのまま引用したものではありません)。

// ① まずプロフィール設定ページを取得する
const xhr = new XMLHttpRequest();
xhr.open("get", "https://example.jp/user/profile");
xhr.responseType = "document";   // レスポンスをテキストではなくDOMとして受け取る
xhr.send();

xhr.onload = function () {
  // ② 正規の画面と同じ手順でアップロード用トークンを読み取る
  const token = xhr.response.getElementById("token").value;

  // ③ cookie(セッションIDを含む)を、そのままPNGファイルの中身にする
  const file = new File([document.cookie], "a.png", { type: "image/png" });

  // ④ 自サイトのアイコン画像アップロード機能へ送信する
  const form = new FormData();
  form.append("uploadfile", file);
  form.append("token", token);

  const xhr2 = new XMLHttpRequest();
  xhr2.open("post", "https://example.jp/user/upload");
  xhr2.send(form);
};

前半5行は、Q社のCSRF対策を通過するためだけに存在する

このスクリプトで注目すべきは、前半がまるごとトークン取得のために費やされていることです。

Q社は、アイコン画像のアップロードに際して「/user/profile で払い出されたトークンと一致すること」を要求していました。これは、別サイトに置かれた偽のフォームから勝手にアップロードされることを防ぐための仕組み、つまりCSRF対策です。

しかし攻撃スクリプトは、被害者のブラウザの中で、被害者と同じオリジンで動いています。ならば正規の画面がやっていることをそのままやればよい。プロフィール設定ページをGETして、その中の token という要素の値を読む。それだけでトークンは手に入ります。

CSRF対策のトークンは、XSSを防ぎません。 XSSが成立した時点で、攻撃者のコードは「ログイン済みの正規利用者」として振る舞えるからです。トークンが守っているのは「別のオリジンから勝手に送られてくるリクエスト」であって、「同じオリジンで動く悪意あるスクリプト」ではありません。

この区別は、実務でセキュリティ要件を読むときにも効きます。「CSRFトークンを入れているので大丈夫です」という説明は、CSRFについては正しくても、XSSについては何も言っていません。

File オブジェクトが持つ意味

③の行も見どころです。document.cookie の文字列を中身とし、ファイル名を a.png、MIMEタイプを image/png としたファイルオブジェクトを作っています。

中身はただのテキストです。PNGファイルではありません。それでもアップロードが成功したのは、問題文にあるとおりWebアプリQがアップロードされた画像ファイルの形式をチェックしていなかったからです。

そして document.cookie が読めてしまったのは、cookieにHttpOnly属性が付与されていなかったからです。RFC 6265は、HttpOnly属性について「cookieのスコープをHTTPリクエストに限定する。とくに、cookieをスクリプトに公開するようなウェブブラウザのAPIといった『非HTTP』のAPI経由でcookieへのアクセスを提供する際に、そのcookieを省略するようユーザーエージェントに指示する」と定めています7。この属性が付いていれば、③の行が掴む文字列からセッションIDだけが外れるので、持ち出しても価値のないものになっていました。

ここで注意したいのは、HttpOnlyが隠すのはその属性が付いたcookieだけだという点です。同じオリジンにHttpOnlyでないcookie(表示設定や計測用のIDなど)があれば、document.cookie はそれらを返し続けます。「HttpOnlyを付ければ document.cookie が空になる」わけではありません。守りたいのはセッションIDなので、セッションIDのcookieに確実に付いているかを個別に確認してください。

7.設問3(2)(3) ── 外へ出ていかない情報持ち出し

設問3(2)は「攻撃者は、アップロードされた情報をどのようにして取得できるか」を50字以内で問います。解答例は「会員のアイコン画像をダウンロードして、そこからセッションIDの文字列を取り出す」です。

思い出してください。問題文の仕様には、こう書かれていました。

アップロードしたアイコン画像は、会員プロフィール設定ページやレビューページに表示される。

つまり、被害者のセッションIDが書き込まれた「アイコン画像」は、サイト上で閲覧できる場所に置かれるわけです。攻撃者は特別なことをする必要がありません。被害者のアイコン画像が表示されているページを開き、その画像のURLを取得するだけです。中身はテキストなので、開けばセッションIDが読めます。

補足すると、この回収が素直にできるのは、被害者のアイコン画像が攻撃者から見えるページ(自分がレビューを投稿している商品のレビューページなど)に出ている場合です。アイコンのURLは会員ごとに決まっているので、一度どこかで見えれば以後はそのURLを直接取りにいけます。いずれにせよ攻撃者側の操作は、正規の閲覧者としてページや画像をGETするだけで、サイトから見れば異常な通信になりません。

被害者のブラウザWebアプリQ(example.jp)攻撃者被害者のブラウザWebアプリQ(example.jp)攻撃者準備段階投稿された文字列をそのまま保存(保存はこれで正しい)被害者がページVを開く★脆弱性はここ★保存済みのタイトルをエスケープせずHTMLに埋め込むscript要素として実行されるdocument.cookie を読みa.png の中身にする形式を確認せず保存アイコン画像として公開回収レビューを15回に分けて投稿(コメント記号でHTMLを飛ばす)1GET ページV2攻撃スクリプトを含むHTML3GET /user/profile4トークンを含むHTML5POST /user/upload(uploadfile=a.png, token=…)6GET 被害者のアイコン画像7セッションIDが書かれたファイル8

出口対策では止まらない

この経路の厄介さは、図にすると明確です。被害者のブラウザは、最初から最後まで正規サイト(example.jp)としか通信していません。

その結果、通信を見張る種類の対策はことごとく空振りします。効くのは最後の1つだけです。

対策 この攻撃に対して 理由
プロキシのURLフィルタリング 効かない アクセス先は業務上正当なECサイトのみ
ファイアウォールの外向き通信の監視 効かない 見慣れないドメインへの通信が発生しない
Content Security Policy の connect-src 制限('self' など同一オリジンを許可する指定) 効かない 2回の通信はどちらも同一オリジン宛てで、ポリシーの許可範囲に収まる
Content Security Policy の script-src 制限('unsafe-inline' を許可しない構成) 効く 埋め込まれたインラインスクリプトがそもそも実行されない

XSSによる情報持ち出しというと「攻撃者のサーバーへcookieが送信される」という図をイメージしがちですが、持ち出し先が被害者サイト自身になり得ることを、この問題ははっきり示しています。書き込み可能で、かつ読み出し可能な場所がサイト内にあれば、そこが受け渡し場所になります。ファイルアップロード機能、公開プロフィール欄、公開メモ欄などが典型です。

CSPについて補足すると、実務でよく使われる指定のうちこの攻撃を確実に止められるのは、connect-src(通信先の制限)ではなく script-src です。script-src 'self' を指定して 'unsafe-inline' を許可しない構成であれば、レビューに埋め込まれたインラインの <script> はそもそも実行されません。CSPを「外部への通信を絞るもの」とだけ理解していると、この違いを見誤ります。

connect-src のほうも、原理的に無力なわけではありません。connect-src 'none' のように同一オリジンへの通信まで禁じる指定や、通信先を特定のエンドポイントだけに絞った許可リストであれば、この2回のXHRも止まります。ただしそこまで絞れるサイトは限られるため、現実的な備えとしては script-src が本命になります。「同一オリジンだからCSPの対象外」なのではなく、「同一オリジンを許可する一般的な指定では通ってしまう」という理解が正確です。

設問3(3):セッションIDで何ができるか

設問3(3)は、取得した情報で何ができるかを40字以内で問います。解答例は「ページVにアクセスした会員になりすまして、WebアプリQの機能を使う」です。

採点講評によれば、この設問の正答率は高かったとのことです。ECサイトでcookieが攻撃者に取得されることの影響は、よく理解されていたと書かれています。

そして問題文の仕様を読み返すと、会員プロフィール機能にはクレジットカード情報を登録するページがあり、ログイン済み会員だけが利用できると明記されていました。なりすましの先に何があるかを、問題文は最初から用意していたわけです。

8.設問4 ── なぜ攻撃者のサイトでは成立しないのか

設問4は、この大問の中でもっとも本質的な問いです。

仮に、攻撃者が用意したドメインのサイトに図4と同じスクリプトを含むHTMLを準備し、そのサイトにWebアプリQのログイン済み会員がアクセスしたとしても、Webブラウザの仕組みによって攻撃は成功しない。この仕組みを40字以内で答えよ。

解答例は「スクリプトから別ドメインのURLに対してcookieが送られない仕組み」です。

同じスクリプトを evil.example に置いて、Q社の会員に踏ませたとします。何が起きるでしょうか。

まず、1回目の通信が空振りします。 evil.example 上のスクリプトから https://example.jp/user/profile へXMLHttpRequestを送っても、これはクロスオリジンのリクエストです。

決定的なのはcookieが付かないことです。図4のスクリプトは withCredentials を指定していないので、クロスオリジンのリクエストにcookieは添付されません。Q社のサーバーから見れば未ログインのアクセスであり、トークンを含んだプロフィール設定ページは返ってきません。スクリプトはトークンを取得できず、そこで止まります。

もう1つ、レスポンスを読めないという壁もあります。Q社が Access-Control-Allow-Originevil.example を許可していなければ、ブラウザはCORSのチェックに失敗してこの通信をネットワークエラーとして扱い、onload は発火せず xhr.response にたどり着けません。ふつうのサイトは見知らぬオリジンにこのヘッダーを返さないので、実際にはここでも止まるはずです。

ただし、Q社がCORSをどう設定していたかは問題文に書かれていません。仮に evil.example からのアクセスを許可していたとしても、読めるのは未ログイン状態のレスポンスであってトークンは入っていないので、やはり攻撃は成立しません。cookieが付かないという1点目だけで、攻撃は止まっています。

どちらか一方でもスクリプトは止まりますが、実際には両方が効いています。

加えて、cookie自体も読めません。 document.cookie が返すのは、そのスクリプトが動いているオリジン(evil.example)のcookieです。example.jp に対して発行されたセッションIDは、そこには入っていません。

IPAの解答例は前者、すなわち「別ドメインのURLに対してcookieが送られない」ことを答えとしています。

ここで、この2つを同じ仕組みとして覚えないでください。別々の仕組みです。

  • document.cookieexample.jp のcookieを返さないのは、cookieがドメインごとに分離して管理されているからです。evil.exampleexample.jp のように別のサイト(別の登録可能ドメイン)どうしであれば、この分離を設定で緩めることはできません。ただし同じドメイン配下のサブドメイン間は別で、cookieの Domain 属性を使えば sub.example.jpexample.jp の間で共有できます7
  • クロスオリジンのXHRにcookieが付かないのは、withCredentials が既定で false だからです。こちらは条件が揃えば変わります。スクリプトが withCredentials = true を指定し、かつcookieの SameSite 属性がクロスサイト送信を許し、かつサーバーが送信元オリジンを明示した Access-Control-Allow-OriginAccess-Control-Allow-Credentials: true を返していれば、cookieは送られ、レスポンスも読めてしまいます

ここで、実際に失敗した理由と、仮に書き換えたら何が必要になるかを分けて押さえてください。

問題文と図4から確実に言えるのは1つです。withCredentials を指定していないのでcookieが添付されず、未ログイン扱いになってトークンが得られない。これだけで攻撃は止まります。

CORSの許可がないこともレスポンスを読めなくする壁になりますが、Q社の設定は問題文に書かれていないので、こちらは「ふつうのサイトならそうなっている」という条件付きの補強と考えてください。

では、攻撃者が withCredentials = true に書き換えたらどうなるか。ここで初めて、もう1つの条件が問題になります。cookieの SameSite がクロスサイト送信を許していなければ、withCredentials を立てても、そのcookieはやはり添付されません。いまのブラウザは SameSite 未指定のcookieを Lax として扱うので、既定では添付されない側に倒れます。

なお、Q社のセッションIDのcookieに SameSite がどう設定されていたかは、問題文に書かれていません。ですから「SameSite のおかげで守られていた」とは言えません。あくまで、credentials付きで攻めてくる相手に対しては SameSite が効きうるという話です。

まとめると、別ドメインから認証済みのレスポンスを読むには、withCredentials = true・cookieの SameSite がクロスサイト送信を許すこと・サーバーがcredentials付きCORSで送信元オリジンを許可すること、の3つが揃う必要があります。ドメインが違えば必ず安全、という話ではありませんが、条件が1つ揃っただけで破れる話でもありません。自社サイトを点検するときは、cookieのドメイン分離に安心するのではなく、CORSの設定(とくにcredentials付きリクエストをどのオリジンに許可しているか)とcookieの SameSite 属性を実際に確認する必要があります。

だから格納型XSSの価値が高い

この設問が教えてくれるのは、攻撃者が「被害者サイトの中で自分のコードを動かす」ことにこだわる理由です。

見た目がそっくりな偽サイトを自分のドメインに用意しても、ブラウザはそれを別のサイトとして扱います。document.cookie から相手サイトのcookieは読めませんし、この設問のスクリプトのように資格情報なしで送るXHRでは、相手サイトから見てログイン状態にすらなりません。攻撃者にとっての価値は、コードが被害者サイトのオリジンで動くことそのものにあります。格納型XSSはまさにそれを実現する手段です。

ただし「別ドメインからは認証付きのリクエストを一切送れない」と読まないでください。cookieの SameSite がクロスサイト送信を許す設定(SameSite=None など)であれば、悪意あるページからcookie付きのリクエストを相手サーバーへ届けること自体はできます。フォームのPOSTなどはその典型で、CORSが制御するのは主に「スクリプトがレスポンスを読めるか」であって、リクエストがサーバーに届くかどうかではありません(ただしカスタムヘッダを付ける場合や application/json で送る場合などはプリフライトが先行するため、許可されなければ本リクエスト自体が送信されません)。これがCSRFという攻撃が成立する理由であり、だからこそCSRF対策のトークンが必要になります。

つまり、別オリジンのスクリプトが既定でできないのは「相手のcookieを読むこと」と「レスポンスを読むこと」であって、「リクエストを送ること」ではありません。

しかも、この2つの「読めない」は強さが違います。document.cookie のドメイン分離はサーバー側の設定で緩められませんが、レスポンスを読めるかどうかは相手サーバーのCORS設定次第です。サーバーが送信元オリジンを許可していれば(資格情報つきなら Access-Control-Allow-Origin に具体的なオリジンを返し、あわせて Access-Control-Allow-Credentials: true を返していれば)、別オリジンのスクリプトでもレスポンスを読めます。自社サイトのCORS設定を確認すべき理由がここにあります。

XSSが特別なのは、こうした条件をすべて飛び越えて、はじめから被害者サイトのオリジンとして振る舞える点にあります。

言い換えれば、XSSは「変な文字が画面に出てしまう不具合」ではなく、攻撃者を自サイトの正規利用者と同じ立場に立たせてしまう不具合です。この認識の差が、優先度の判断を分けます。

9.効いた対策と、効かなかった対策

ここまでを1枚の表にまとめます。この記事でいちばん持ち帰ってほしい部分です。

Q社にあった/なかった仕組み この攻撃に対して 理由
HTTPSでの通信 効かなかった 通信路の保護であり、ページに埋め込まれたスクリプトとは無関係
レビュータイトル50字・詳細300字の入力文字数制限 効かなかった 15回に分割して投稿され、間のHTMLはJavaScriptのコメントで読み飛ばされた
アップロード用トークン(CSRF対策) 効かなかった 同一オリジンのスクリプトは正規の手順でトークンを取得できる
ログイン必須のアップロード機能 効かなかった 実行しているのはログイン済みの被害者自身のブラウザ
出力時のエスケープ(なかった) 効く(根本的解決) 投稿された文字列がHTMLとして解釈されなくなり、そもそもスクリプトが動かない
cookieのHttpOnly属性(なかった) 効く(保険的対策) document.cookie からセッションIDが読めなくなる
アップロードファイルの形式確認・再エンコード(なかった) 効く(保険的対策) 生のテキストはPNGとして保存できず、メタデータに隠された文字列も再エンコードで消える。ただし画像のピクセルとして符号化されると残る

上の4つは、この攻撃に対しては何の働きもしませんでした。下の3つをIPA「安全なウェブサイトの作り方」の区分に当てはめると、最初の1つが根本的解決、残り2つが保険的対策です5

ひとつ補足すると、上の4つが「IPAが対策として認めていないもの」という意味ではありません。IPAの区分は根本的解決と保険的対策の2つで、入力値の内容チェックはむしろ保険的対策として挙げられています。ただしそこには「この対策が有効となるのは限定的です」という但し書きが付いており、今回はまさにその限定に当たった、ということです。

そして重要なのは、下3つはどれか1つでもあれば、この問題で実行された攻撃の連鎖は途切れていたという点です。

  • エスケープしていれば、そもそもスクリプトが実行されない
  • HttpOnlyが付いていれば、スクリプトは実行されるがcookieを読めない
  • 形式を確認していれば、cookieは読めるが、生のテキストをPNGとしてアップロードできない

多層防御が意味を持つのは、まさにこういう場面です。ただし、保険的対策は根本的解決の代わりにはなりません。HttpOnlyを付けていても、XSSが残っていれば攻撃者は被害者のブラウザで任意の操作(商品の購入、登録情報の変更など)を実行できます。セッションIDを盗む必要すらない攻撃は、いくらでも設計できます。

なぜ「形式確認」だけでなく「再エンコード」まで書いたのか

表の最終行を「形式確認・再エンコード」と2段構えで書いたのには理由があります。

問題文のQ社は画像ファイルの形式をまったく確認していなかったので、形式チェックを入れるだけでもこの攻撃は止まります。しかし、それは攻撃者が生のテキストを送ってきた場合に限った話です。攻撃者は正当なPNGファイルを組み立て、そのメタデータ領域などに文字列を忍ばせることもできます。この場合「形式が正しいか」の確認は通ってしまいます。

したがって、この経路を狭めるなら、アップロードされた画像をサーバー側で再エンコードして配信する(元のバイト列をそのまま保存しない)ところまでやる必要があります。

ただし、再エンコードでも塞ぎきれるわけではありません。攻撃者がセッションIDを画像のピクセルそのものとして描き込んで正当なPNGを作った場合、通常の再エンコードはその見た目の情報を保持するので、攻撃者はダウンロードして読み取れます。再エンコードが止めるのは、生のバイト列を送る変種とメタデータ領域に隠す変種であって、画像の中身として符号化された情報ではありません。

つまり「書き込めて、かつ読み出せる場所」がサイト内にある限り、そこを持ち出し経路として完全に閉じることはできません。これは保険的対策の性質をよく表しています。保険的対策は「いま観測された攻撃」を止めるだけでは足りず、攻撃者が手口を少し変えたときにも成立するかまで見て設計する必要があります。そしてどれだけ重ねても、根本的解決である出力時のエスケープの代わりにはなりません。

なお、アップロードされたファイルを本体とは別のドメインから配信するという設計もよく勧められますが、これはこの持ち出し経路への対策ではありません。攻撃者は別ドメインからでも同じように画像をダウンロードできますし、同一オリジンポリシーは公開されているバイト列を秘密にする仕組みではないからです。別ドメイン配信が効くのは、アップロードされたファイル自体が能動的なコンテンツとして被害者サイトのオリジンで実行されてしまう脅威(HTMLやSVGをアップロードされる類)に対してです。別の脅威への対策として、切り分けて理解してください。

ファイルを送信側の申告(拡張子やContent-Type)ではなく中身で判断するという考え方自体は、Webに限らず必要になる基本動作です。ただし「中身で判断する」の中身は形式によって違います。PNGのように先頭バイト列に決まった並びを持つ形式なら、それを確認できます。一方でCSVには標準化された署名がなく、先頭のBOMは文字エンコーディングを示すだけで中身がCSVである証拠にはなりません。CSVファイルの取扱いで扱ったように、CSVの場合は想定する方言でパースが通るか、期待するスキーマと件数の上限に収まるかを確認することになります。

10.自分のコードで確認する観点

この大問をコードレビューのチェックリストに落とすと、次のようになります。会員機能や投稿機能を持つWebアプリケーションであれば、そのまま使えます。

  1. 出力時のエスケープが、すべての出力箇所で効いているか。 テンプレートエンジンの自動エスケープを明示的に外している箇所(raw| safedangerouslySetInnerHTML など)を全件洗い出し、それぞれについて外してよい理由を説明できるか確認する
  2. 入力側の制限をXSS対策として数えていないか。 文字数制限や入力可能文字種の制限は仕様上の要件であって、XSSの根本的解決ではない
  3. 入力時にエスケープしていないか。 入力の役割は仕様外の値を弾く検証であって、エスケープではない。入力時点では出力先(HTML・CSV・JSON・メール・ログ)が決まっていないため、入力時のエスケープは二重エスケープとデータ破壊を招く。SQL文の組み立ても同じで、根本的解決はプレースホルダである
  4. cookieに HttpOnlySecureSameSite が付いているか。 セッションIDのcookieは特に確認する
  5. アップロードされたファイルの形式を、拡張子やContent-Typeではなく中身で確認しているか。 送信側が申告した情報は検証の材料にならない。ただし形式チェックだけでは、正当な画像のメタデータ領域にデータを忍ばせる手口は通ってしまう
  6. アップロードされた画像を再エンコードして配信しているか。 元のバイト列をそのまま返していないか。ただし再エンコードでもピクセルとして符号化された情報は残るため、「書き込めて読み出せる場所」は完全には塞げないと理解しておく。なお別ドメインからの配信は、アップロードされたファイルが自サイトのオリジンで実行される脅威への対策であって、この持ち出し経路への対策ではない
  7. CSPでインラインスクリプトを止められる構成になっているか。 判断基準は「'unsafe-inline' が書かれているか」ではなく「インラインスクリプトが実際に許可されているか」。script-src に nonce や hash の指定があると、CSP Level 2 以降のブラウザは 'unsafe-inline' を無視するため、'unsafe-inline' と nonce を併記する後方互換のポリシーでは、nonce を持たない注入スクリプトはブロックされる
  8. トークンが「何を」守っているのか説明できるか。 CSRF対策のトークンはXSSを防がない。それぞれ別の対策が必要である

とくに1番目は、実際の調査で見つかる典型パターンです。フレームワークが自動エスケープしてくれているから安全だと考えていたが、「HTMLをそのまま入れたい」という個別要件のために1か所だけ自動エスケープを外していた、というケースは珍しくありません。

おわりに ── この問題が問うている本当の能力

ここまで見てきた対策の一覧は、冒頭の「まず結論」にまとめてあります。最後に、この問題そのものについて一つだけ書いておきます。

試験問題としてよくできているのは、「XSSを知っているか」ではなく「手元にある対策のうち、どれが実際に効いているのかを見分けられるか」を問うている点です。

Q社は何もしていなかったわけではありません。HTTPSで通信し、入力に文字数制限を設け、アップロードにはトークンを要求し、機能はログイン済み会員に限定していました。並べれば、それなりに対策された作りに見えます。それでも全部すり抜けられました。逆に、欠けていた3つはどれも地味で、リリースノートに書くような機能ではありません。

セキュリティの議論が難しいのは、対策の数と、防御の強さが比例しないからです。「何をやっているか」ではなく「その対策は、どの攻撃の、どの段階を止めるのか」を一つずつ言えるかどうか。これは資格試験のためだけの能力ではなく、セキュリティ対策の説明を受ける側にとっても、実装する側にとっても、いちばん必要な判断力です。

関連する相談領域

合同会社小村ソフトでは、会員機能や投稿機能を持つWebサイトの制作と、既存のWebアプリケーションで同じ穴が空いていないかの設計レビューを扱っています。

参考リンク

  1. IPA 独立行政法人情報処理推進機構, 問題冊子・配点割合・解答例・採点講評(2023年度、令和5年度) 所収「令和5年度 秋期 情報処理安全確保支援士試験 午後 問題」。Q社のWebアプリQの機能(会員登録、ログインとcookieによるセッションIDの払出し、商品レビュー機能のレビュータイトル50字・レビュー詳細300字の入力文字数制限、会員プロフィール機能のアイコン画像アップロードとクレジットカード情報登録、アイコン画像アップロードが画像ファイルとトークンをパラメータとすること、アップロードしたアイコン画像が会員プロフィール設定ページやレビューページに表示されること)、ページVで16件のレビューが2件しか表示されない事象、ページVのHTMLに会員Aによる15件の投稿と長いスクリプトが含まれていたこと、抽出されたスクリプトの内容、そしてWebアプリQに会員が入力したスクリプトが実行されてしまう脆弱性があったこと、cookieにHttpOnly属性を付与していなかったこと、アップロードされた画像ファイルの形式をチェックしていなかったことについて。設問1から設問4の設問文もこの冊子による。  2

  2. IPA 独立行政法人情報処理推進機構, 試験に関するよくある質問. 当機構で公表している過去の試験問題の使用に関し、法令に特別の定めがある場合を除き許諾や使用料は必要ないこと、ただし著作権は放棄していないこと、出典を「年度、期、試験区分、時間区分、問番号等」の形式で明記する必要があること(例として「出典:平成31年度 春期 基本情報技術者試験 午前 問1」が示されている)、および問題の一部を改変している場合はその旨も明記する必要があることについて。 

  3. IPA 独立行政法人情報処理推進機構, 令和5年度 秋期 情報処理安全確保支援士試験 解答例. 問1の出題趣旨(Webアプリケーションプログラムの脆弱性を悪用されたことによるインシデント対応を題材に、HTMLやECMAScriptから悪用された脆弱性と問題点を読み解き、対策を立案する能力を問う)、および各設問の解答例(設問1(1)は「イ 格納型XSS」、設問1(2)は「レビュータイトルを出力する前にエスケープ処理を施す。」、設問2は「HTMLがコメントアウトされ一つのスクリプトになるような投稿を複数回に分けて行った。」、設問3(1)は「XHRのレスポンスから取得したトークンとともに、アイコン画像としてセッションIDをアップロードする。」、設問3(2)は「会員のアイコン画像をダウンロードして、そこからセッションIDの文字列を取り出す。」、設問3(3)は「ページVにアクセスした会員になりすまして、WebアプリQの機能を使う。」、設問4は「スクリプトから別ドメインのURLに対してcookieが送られない仕組み」)について。 

  4. IPA 独立行政法人情報処理推進機構, 令和5年度 秋期 情報処理安全確保支援士試験 採点講評. 問1全体の正答率が平均的であったこと、設問1(1)について「スクリプトでDOMを使用していたことからか、”DOM Based XSS”と誤って解答する受験者が散見された」こと、脆弱性は特徴や対策方法まで含めて正確に理解してほしいという指摘、設問2について「”開発者ツールで入力制限を削除してから投稿した”のように、確認が不足していると考えられる解答が一部に見られた」こと、攻撃者の残した痕跡を注意深く確認し攻撃の方法を正確に把握する能力を培ってほしいという指摘、そして設問3(3)の正答率が高く、ECサイトにおいてcookieが攻撃者に取得されることの影響がよく理解されていたことについて。 

  5. IPA 独立行政法人情報処理推進機構, 安全なウェブサイトの作り方. ウェブサイトの脆弱性11種類について脅威と対策を「根本的解決」(脆弱性の原因そのものを取り除く実装)と「保険的対策」(脆弱性が残った場合に攻撃の成功率や被害を下げる対策)に分けて示していること、クロスサイト・スクリプティングの根本的解決としてウェブページに出力する全ての要素に対してエスケープ処理を施すことが挙げられていること、および付属のセキュリティ実装チェックリストと別冊「安全なSQLの呼び出し方」「ウェブ健康診断仕様」について。  2 3

  6. IPA 独立行政法人情報処理推進機構, 安全なウェブサイトの作り方 - 1.1 SQLインジェクション. SQLインジェクションの根本的解決として「SQL文の組み立ては全てプレースホルダで実装する」が挙げられ、静的プレースホルダ(プリペアドステートメント)のほうが原理的に脆弱性が生じ得ないとされていること、文字列連結でSQL文を組み立てる場合の実装として「SQL文の組み立てを文字列連結により行う場合は、エスケープ処理等を行うデータベースエンジンのAPIを用いて、SQL文のリテラルを正しく構成する」と書かれており、エスケープがSQL文を構成するリテラルの生成に対して行われるものであること(入力を受け取った時点ではないこと)、あわせて根本的解決として「ウェブアプリケーションに渡されるパラメータにSQL文を直接指定しない」が、保険的対策として「エラーメッセージをそのままブラウザに表示しない」「データベースアカウントに適切な権限を与える」が挙げられていることについて。 

  7. IETF, RFC 6265: HTTP State Management Mechanism, Section 4.1.2.6 “The HttpOnly Attribute”. HttpOnly属性がcookieのスコープをHTTPリクエストに限定すること、とくにcookieをスクリプトに公開するようなウェブブラウザのAPIといった「非HTTP」のAPI経由でcookieへのアクセスを提供する際に、そのcookieを省略するようユーザーエージェントに指示することについて。あわせてSecure属性(Section 4.1.2.5)がセキュアなチャネルでのみcookieを送信するよう制限するものであることについて。  2

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

受託開発・運用保守の契約はどう結ぶべきか ── IPA「モデル取引・契約書」に学ぶ準委任と請負の使い分け

システム開発を外部に委託するとき、契約はどう結ぶべきでしょうか。IPAが公開する「情報システム・モデル取引・契約書」をもとに、多段階契約の考え方、準委任と請負の違い、運用保守契約で決めておくべきことを、発注側にも分かりやすく解説します。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

HP制作

会員機能や投稿機能を持つWebサイトでは、出力時のエスケープやcookieの属性設定といったこの記事の論点が、そのまま制作品質に直結するためです。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

この問題のXSSはなぜ「格納型」なのですか。スクリプトはDOMを操作しているのに、DOM Based XSSではないのですか。
脆弱性の種類は、攻撃者の文字列が「どこで」実行可能なコードに変わるか、つまり脆弱な出力箇所(シンク)がサーバー側にあるかブラウザ側にあるかで決まります。この問題では、攻撃者が投稿したレビュータイトルをサーバーがそのままHTMLに埋め込んで返していました。シンクはサーバーの出力処理にあり、その文字列は保存されて以後ページを開いた全員に配られるので格納型XSSです。DOM Based XSSは、ブラウザ上のJavaScriptがinnerHTMLへの代入などを通じて値を実行可能にしてしまう類型を指します。埋め込まれたスクリプトがXMLHttpRequestやgetElementByIdといったDOMのAPIを使うかどうかは、脆弱性の種類とは関係ありません。なお、サーバーが保存済みの値を無害なテキストやJSONとして返していても、それをブラウザ側のJavaScriptがinnerHTMLに渡せばDOM Based XSSになります。レスポンスに文字列が見えるかどうかではなく、どこで実行可能になったかで判定してください。IPAの採点講評でも、スクリプトでDOMを使用していたことからかDOM Based XSSと誤って解答する受験者が散見された、と指摘されています。
レビュータイトルに50字の入力文字数制限があったのに、なぜ長いスクリプトが実行できたのですか。
攻撃者は1回の投稿に収まる長さに分割し、複数回に分けて投稿しました。1件目はタイトルの途中でscriptタグを開き、タイトル末尾でJavaScriptのブロックコメント(スラッシュとアスタリスク)を開いておきます。2件目の先頭でそのコメントを閉じてから次の命令を書き、末尾でまたコメントを開く、という書き方を繰り返します。こうすると、投稿と投稿の間にあるHTML(divタグなど)はすべてJavaScriptのコメントの中に入るため無視され、分割された断片だけがつながって1本のスクリプトとして実行されます。入力文字数制限は1件あたりの長さを制限するだけで、投稿の回数は制限していなかったということです。
盗んだセッションIDは、どこへ送られたのですか。
外部のサーバーには送られていません。スクリプトはcookieの中身をそのままファイルの中身にして、名前を「a.png」、MIMEタイプを「image/png」として、被害者が利用しているECサイト自身の会員アイコン画像アップロード機能へ送信しました。アップロードされたアイコン画像はレビューページなどに表示されるため、攻撃者はその画像を普通にダウンロードして、中身の文字列からセッションIDを取り出せます。被害者のブラウザは正規サイトとしか通信しておらず、外部への不審な通信が発生しないため、プロキシのURLフィルタリングやファイアウォールの外向き通信の監視では気付けない経路になっています。
アップロードにはトークンが必要だったはずですが、なぜ攻撃が成立したのですか。
そのトークンは、別サイトからの勝手なリクエストを弾くための仕組み(CSRF対策)であって、同じサイト上で動くスクリプトを想定した対策ではないからです。攻撃スクリプトは、まずプロフィール設定ページへXMLHttpRequestでアクセスし、正規の画面と同じようにトークンを受け取ってから、そのトークンを添えてアップロードを実行しています。XSSによって攻撃者のコードは被害者と同じオリジンで動いているので、正規の利用者にできることは何でもできます。CSRF対策のトークンはXSSを防ぎません。
SQLインジェクションでは入力のエスケープが大事だと聞きます。XSSだけが出力時なのですか。
いいえ、SQLインジェクションでも「入力時のエスケープ」は対策ではありません。IPAが根本的解決として挙げているのは「SQL文の組み立ては全てプレースホルダで実装する」ことです。文字列連結でSQL文を組み立てる場合の代替としてエスケープ処理が挙げられていますが、それも「SQL文のリテラルを正しく構成する」ため、つまりSQL文を組み立てる瞬間に行うものであって、入力を受け取った時点ではありません。共通する原則は「エスケープは、データがどの文法の世界へ出ていくかが決まる場所で行う」です。HTMLへ出るならHTMLのエスケープ、SQL文の一部になるならプレースホルダ、シェルコマンドの一部になるならシェルの流儀、という具合です。入力時にエスケープしてはいけない理由は、その時点では出力先が決まっていないからです。同じデータがHTMLページにもCSVにもJSONにも通知メールにもログにも出ていくため、入力時にHTMLエスケープをかけるとCSVに実体参照がそのまま出たり、データベースの中身が元の入力と違うものになって検索や集計が狂ったりします。なお入力側で何もしないという意味ではありません。入力側の役割はエスケープではなく検証(バリデーション)で、仕様上ありえない値を弾くことです。
この問題から実務に持ち帰るべき対策は何ですか。
根本的解決は、レビュータイトルを含むすべての利用者入力について、HTMLへ出力する直前にエスケープ処理を施すことです。そのうえで保険的対策として、セッションIDのcookieにHttpOnly属性を付けてスクリプトから読めなくすること、アップロードされた画像をサーバー側で再エンコードして元のバイト列を残さないこと、Content Security Policyでインラインスクリプトの実行を止められる構成にしておくことが挙げられます。この問題のQ社は、このいずれか1つでも実装していれば攻撃の連鎖がどこかで途切れていました。ただし保険的対策は攻撃者が手口を変えれば回避されうるものなので、根本的解決であるエスケープの代わりにはなりません。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る