水無月ばけらのえび日記

bakera.jp > 水無月ばけらのえび日記 > 2012年のえび日記 > 2012年4月 > 2012年4月16日(月曜日)

2012年4月16日(月曜日)

平成24年度春期情報セキュリティスペシャリスト試験のXSS問題

公開: 2012年4月30日13時35分頃

今年の春の情報処理技術者試験の問題が公開されていますね。

面白いと思ったのは「情報セキュリティスペシャリスト試験」の午後Ⅰの問1。こういうコードがあります。

out.println("<a name=\"#\" onclick=\"alert('" + escape(word) + "')\">");
out.println("previous search word");
out.println("</a>");

以上、平成24年度 春期 情報セキュリティスペシャリスト試験 午後Ⅰ 問題 図4(続き) より

やや読みにくいですが、こんな感じのHTMLが出力されるイメージになります。

<a name="#" onclick="alert('word')">
previous search word
</a>

変数wordには入力されたキーワードが入っていて、関数escapeはこういう内容。

private static String escape(String message){
// この関数は、messageに含まれる文字を次のように置換したString型の文字列を返す。
// 「&」→「&amp;」「<」→「&lt;」「>」→「&gt;」「"」→「&quot;」
(省略)
}

以上、平成24年度 春期 情報セキュリティスペシャリスト試験 午後Ⅰ 問題 図4(続き) より

しかしこれではXSS脆弱性があるので、新たにescape2という関数を作ってそれを呼ぶことにしたそうです。

escape2関数では, 意図したスクリプトが生成されるように, まず, スクリプト言語の文法上必要なエスケープ処理を行った後, 更にescape関数でエスケープ処理を行う。

private static String escape(String message){
// この関数は、messageに含まれる文字を次のように置換したString型の文字列を生成し、
// その文字列をescape関数に渡し、その結果のString型の文字列を返す。
// 「[ d ]」→「[ e ]」「[ f ]」→「[ g ]
(省略)
}

以上、平成24年度 春期 情報セキュリティスペシャリスト試験 午後Ⅰ 問題 図5 より

このd~gの空欄について、d,fには置換の対象文字を、e,gには置換後の文字を答えよ、というのが問題です。

結構な難問だと思います。

まずescape2という関数名を全力で却下したくなりますが、「適切な名前をつけると問題のヒントになってしまうので、あえてこうしているに違いない」と自分に言い聞かせて落ち着きましょう。

問題は、この関数がどのように使われる想定なのかよく分からないことです。汎用的に使われるのだとすると、問題の箇所でうまく動くだけでは駄目で、たとえばJavaScriptのリテラルが二重引用符で括られていた場合もうまく処理できないとまずいでしょう。

しかし回答欄を見ると、エスケープできる対象文字は2つしかありません。これでは汎用的な処理は無理なので、このコードに限定した話と考えるべきなのでしょう。つまり、escape2は「二重引用符でくくられたHTMLの属性値リテラル内の、単引用符でくくられたJavaScriptの文字列リテラル」に限定して処理できれば良いのだと割り切るわけです。

しかし、そう割り切っても、2つではちょっと足りない気がするのですね。

まずシングルクォート「'」のエスケープが必要なのは明らかです。これを「\'」や「\x27」や「\u0027」にエスケープします。

そしてバックスラッシュ「\」を「\\」や「\x5c」や「\u005c」にエスケープします。これをエスケープしないと、「\'」という文字列を入れられたときや、末尾に \ を入れられたときに不都合がおきます。

さらに、改行を「\n」などにエスケープする必要があります。これをエスケープしないと、文字列リテラルが終了していないとみなされてエラーになります。たとえばIE9の場合だと、コンソールに「終了していない文字列型の定数です。」というエラーが記録され、スクリプトは動作しません。

とはいえ、改行のエスケープを怠っても任意のスクリプトが動作するということはありません。実務上は「動かなくなる」というのは不具合とみなされると思いますが、純粋に問題への解答としては、改行のエスケープはなくても良いといえるのかもしれません。

というわけで、「このコードに限定した話」かつ「攻撃さえ防げれば良い」という立場をとり、シングルクォートとバックスラッシュのみをエスケープするのが正解……。

……と言いたいところでしたが、ちょっと待ってください。「このコードに限定した話」かつ「攻撃さえ防げれば良い」という立場を取るのだとすると、バックスラッシュのエスケープは本当に必要なのでしょうか?

シングルクォートを「\'」にエスケープするのなら、バックスラッシュのエスケープは必須になります。ユーザーが「\');alert(document.cookie)//」を入力したとき、バックスラッシュのエスケープがないと、問題箇所はこうなります。

onclick="alert('\\');alert(document.cookie)//')"

この場合、クリックすると1度「\」が表示されたあと、続けてCookieの値が (もしあれば) 表示されます。つまり、任意のスクリプトが動作します。

しかし、シングルクォートを「'\」ではなく「\u0027」にエスケープしていると、こうなります。

onclick="alert('\\u0027);alert(document.cookie)//')"

「\u0027);alert(document.cookie)//」という文字列が表示されるだけです。「'」が「\u0027」に化けて表示される不具合がありますが、任意のスクリプトが動作しているわけではありません。

シングルクォートを「\u0027」にエスケープすれば、\' を入力されても \\u0027 となって \u0027 と表示されるだけです。意図とは異なりますが、任意のスクリプトは動作させられません。文字列の末尾に \ を入力されると、リテラルの終端の ' が壊れてスクリプトエラーになりますが、これは改行を入れたときと同じ事です。「このコードに限定した話」であれば、その後はすぐに改行が出現し、改行の前にユーザーが何かを入れる余地はありません。

任意のエスケープシーケンスを入れることができるという問題がありますが、文字列リテラルの中に何があっても、スクリプトの実行には至らないはずです。何か見落としがある (突破方法がある?) かもしれませんが、どうでしょうか……?

まとめると、こういうことです。

通常の実装では、エスケープする文字の種類が2種類に制限されることはないと思いますので、不具合がないようにしっかり4種類エスケープすれば良いと思います。あるいは、英数字以外の全てを \uXXXX の形式にエスケープするようなアプローチでも良いでしょう。

ところで、不具合と言えば、問題のコードにはエスケープとは関係ない不具合らしき点があります。問題箇所のHTMLはこうなるのですが、

<a name="#" onclick="alert('word')">previous search word</a>

a要素にhref属性がついていません。一応クリックすればスクリプトは動きますが、特殊なスタイルを適用しなければクリックできそうな見た目になりませんし、キーボード操作もできません。name属性に "#" という値を指定するのも妙な話です。

これはまあ、hrefとnameの取り違えでしょう。出題者はこのコードを実際に動かしていないのではないでしょうか?

※ちなみにHTML4のa要素のname属性の値はIDではなくCDATAなので、"#" という値はいちおう正当です。HTML5のa要素にはname属性はありません。

関連する話題: Web / セキュリティ

最近の日記

関わった本など