[PR]小規模ECサイトに最適なWAF、SiteGuard Lite

徳丸浩の日記


2009年10月09日 [php][xss]

_htmlspecialcharsのShift_JISチェック漏れによるXSS回避策

このエントリでは、PHPのhtmlspecialchars関数の文字エンコーディングチェック不備をついたクロスサイト・スクリプティング(XSS)脆弱性について、PHP側のパッチが提供されない状況での回避策について説明します。

何が問題か

PHPにおいて、XSS対策にはhtmlspecialcharsによって記号をエスケープすることが行われますしかし、htmlspecialcharsを利用していても、Shift_JISの先行バイトを利用して、XSSが発生する場合があります。

例えば、以下のようなINPUTがあり、外部から属性値を変更できる箇所が2カ所以上ある(以下の例では、AAAとBBB)とします。

<INPUT name="AAA" value="BBB">

ここで、AAAとBBBにそれぞれ以下のような値を与えます。文字エンコーディングはShift_JISとします。

AAA: %F1
BBB: onmouseover%3dalert(document.cookie);//

この場合、\xF1と後続の「"」が、合わせて一文字と見なされ(「■」で表します)以下のようなHTMLが与えられたとブラウザにより認識されます。

<INPUT name="■ value="onmouseover=alert(document.cookie);//">

すなわち、value=で与えたはずの「onmouseover=...」が属性値をはみ出し、イベントハンドラと見なされます。これにより、XSSが発生すると言う問題です。

一般的に、このような「Shift_JISの先行バイト」を用いたXSSは、\xF1以外にも、0x81~0x9Fおよび0xE0~0xFCの範囲のバイトが使用できます。htmlspecialchars(PHP5.2.5以降)は、これら単独の先行バイトをチェックしますが、なぜか0xF0~0xFCについてはチェックされず素通しになっています。このチェックもれが問題になっています。

今までの流れ

最近この問題が話題になっている流れを時系列に示します。

  1. 元々、htmlspecialcharsは文字エンコーディングの妥当性チェックをほとんど行っていませんでした。その当時の事情は、寺田さんの調査「htmlspecialcharsと不正な文字の話」に詳しく書かれています。
  2. その後、最近になって、「PHP5.2.5以降では文字エンコーディングのチェックが入っているけど、ちょっと中途半端だ」という調査結果を私が「htmlspecialcharsは不正な文字エンコーディングをどこまでチェックするか」にて指摘しました。
  3. このブログで、「XSSの攻撃に対する抜けが生じるかと言えば、突破の方法はちょっと思いつきません」と書いていたところ、id:t_komuraさんが「Shift_JIS では、htmlspecialchars() を使用しても XSS が可能な場合がある」にて、その突破方法を報告して下さっています。
  4. これに対して、id:IwamotoTakashiさんが、「htmlspecialcharsのパッチ私案」にて対策パッチを公開され、バグレポートを提出されましたが、「htmlspecialcharsに関する残念なお知らせ」で報告されているように、現時点ではPHP開発チームから却下されている状況です。
  5. このあたりから、一連の流れが広く知られるようになって、「もっと効果的な訴求方法があるよ」とか、海老原昂輔さんからもバグレポートが投稿されるなどの働きかけが始まっているようです。海老原さんのレポートには私のエントリも英訳されていて、本当にありがとうございます。
  6. さて、PHP本体が修正されるのが一番よいのですが、このエントリでは、PHP側の修正前に、どのようにこの問題に対策すればよいかを説明します(←イマココ)

問題が発生する条件

幸いなことに、この問題が発生するためには、色々条件がつきます。それを以下に示します。すべてAND条件です。

  1. PHPが内部で扱う文字エンコーディングがShift_JISである
  2. 入力から出力までの過程で文字エンコーディングが変換されない
  3. 入力値のバリデーションとして文字エンコーディングを検査していない

すなわち、1.と2.を合わせますと、入口・処理・出口まで一貫してShift_JISで扱っている、という条件が攻撃には必須ということになります。

入口(HTTP Request)と出口(HTTP Response)がShift_JISというサイトは珍しくなくて、ケータイサイトは大抵こうなっています。問題は、内部処理がShift_JISというところです。PHPはShift_JISの文字列リテラルをうまく扱えないので、いわゆる5C問題が発生します。そのため、「PHPの内部エンコーディングではShift_JISを避けよう」というノウハウがかなり普及しているのではないかと思います。このあたりの詳しい説明は、「Shift_JISを利用することの是非」や「第8回■主要言語の文字エンコーディングの対応状況を押さえる(ITpro)」をご覧下さい。

既存サイトの回避策

既存サイトで、一貫してShift_JISで処理している場合もやはりあるでしょう。その場合にどうこの問題に対処すればよいでしょうか。

既に稼働しているサイトの文字エンコーディングを変更するとなると、改修もさることながら、サイト全体に対するテストをしっかりやらないといけないので、そう簡単にはできないでしょう。また、文字エンコーディングのバリデーション処理を追加するのは、とてもよいことではありますが、やはりコードの改修・追加とテストが大変です。

最終的には文字エンコーディングのバリデーション処理の追加を推奨しますが、それがすぐにできない場合の暫定対応として、入口でShift_JIS→Shift_JISの変換をする、という方法があります。php.iniに以下のような設定をすることにより、入力データをShift_JIS→Shift_JISの変換を指示します。

[mbstring]
mbstring.language = Japanese
mbstring.internal_encoding = sjis-win
mbstring.http_input = sjis-win
mbstring.http_output = sjis-win
mbstring.encoding_translation = On

Shift_JIS→Shift_JISの変換というと、何もしないのではないかと思われるかもしれませんが、この指定により、不正なShift_JISに相当するバイトは除去されます。このため、Shift_JISの先行バイトを使用したXSS攻撃も防止することができます。

PHPによるアプリケーション開発に、そもそもShift_JISを使うこと自体が好ましくありませんので、あくまで暫定的・緊急に問題を回避するための手法として紹介します。また、設定変更後はサイトの動作検証を行って下さい。

新規開発する場合はどうか

これから新規開発するサイトの場合はどうでしょうか。この場合は、ぜひ以下の二点を実施して下さい

  1. アプリケーション仕様として適切な文字エンコーディングを選択する
  2. 文字エンコーディングのバリデーションを実施する

これらの内容は既にITproに詳しく書いていますので、そちらを参照して下さい。

また、これらの回に先立ち、なぜそうすべきかも説明しているので合わせてご覧いただければと思います。

まとめ

PHPのhtmlspecialcharsがShift_JISの先行バイトをきちんとチェックしていないために、半端な先行バイトを悪用したXSSが可能となることが指摘されています。これに対して、問題が発生する条件と、暫定的な対策、根本的な対策を説明しました。

文字エンコーディングの問題は、後から対策しようとするとやっかいですが、上流工程で考慮しておけば大幅に労力を削減することができます。この機会に、文字エンコーディングの問題に関心を持っていただければ幸いです。

[PR]Webアプリケーションのセキュリティ対策はHASHコンサルティングまで

本日のツッコミ(全1件) [ツッコミを入れる]

_ 海老原昂輔 [ご紹介いただいてありがとうございます。今回僕が行動できたのも、徳丸さんのわかりやすいエントリがあってこそです。こちら..]


2009年09月30日 [php][security][xss]

_htmlspecialcharsは不正な文字エンコーディングをどこまでチェックするか

このエントリでは、PHPのhtmlspecialchars関数の第三パラメータ(文字エンコーディング指定)により、どこまで文字エンコーディングの妥当性チェックをしているかを報告します。 2007年4月に、寺田氏(id:teracc)の素晴らしいエントリ「htmlspecialcharsと不正な文字の話」により、htmlspecialcharsは、第三パラメータの文字コードを実質的に無視しており、不正な文字エンコーディングが指定された場合、その文字はチェックされずにすり抜けてしまうという結果が報告されています。

寺田氏のエントリには、検証に使用したPHPのバージョンが明記されていませんが、エントリが書かれた時期から考えて、PHP5.2.1またはそれ以前のバージョンと考えられます。以下は、PHPに同梱されるnews.txtからの引用です。

08 Feb 2007, PHP 5.2.1
03 May 2007, PHP 5.2.2

しかし、その後PHP5.2.5に至って、文字エンコーディングのチェックがなされるように改善されました。PHP5.2.5のnews.txtから引用します。

08 Nov 2007, PHP 5.2.5
【中略】
 - Fixed htmlentities/htmlspecialchars not to accept partial multibyte sequences. (Stas)

部分的なマルチバイト・シーケンスを受け付けないようにしたとのことですので、本当にそうなっているかを以下のコードを用いて検証しました。

<?php
function test($s, $enc) {
  $e = htmlspecialchars($s, ENT_QUOTES, $enc);
  echo bin2hex($s) . ':' . $s . ' -> ';
  echo bin2hex($e) . ':' . $e . "\n";
}

  echo phpversion() . "\n";

  test("\xC0\xAF", 'UTF-8');                  // 「/」の冗長表現(2バイト)
  test("\xE0\x80\xAF", 'UTF-8');              // 「/」の冗長表現(3バイト)
  test("\xF0\x80\x80\xAF", 'UTF-8');          // 「/」の冗長表現(4バイト)
  test("\xF8\x80\x80\x80\xAF", 'UTF-8');      // 「/」の冗長表現(5バイト)
  test("\xFC\x80\x80\x80\x80\xAF", 'UTF-8');  // 「/」の冗長表現(6バイト)
echo "-------------------------------\n";
  test("\xC0\xBC", 'UTF-8');                  // 「<」の冗長表現(2バイト)
  test("\xE0\x80\xBC", 'UTF-8');              // 「<」の冗長表現(3バイト)
  test("\xF0\x80\x80\xBC", 'UTF-8');          // 「<」の冗長表現(4バイト)
  test("\xF8\x80\x80\x80\xBC", 'UTF-8');      // 「<」の冗長表現(5バイト)
  test("\xFC\x80\x80\x80\x80\xBC", 'UTF-8');  // 「<」の冗長表現(6バイト)
echo "-------------------------------\n";
  test("A\xC2", 'UTF-8');                     // C2 (2バイトパターンなのに1バイト)
  test("A\xC2<", 'UTF-8');                    // C2 に < が続く
  test("A\xC2/", 'UTF-8');                    // C2 に / が続く
  test("A\xE6\xBC", 'UTF-8');                 // E6 BC (3バイトパターンなのに2バイト)
  test("A\xE6\xBC<", 'UTF-8');                // E6 BC に < が続く
  test("A\xE6\xBC/", 'UTF-8');                // E6 BC に / が続く
echo "-------------------------------\n";
  test("A\x8A", 'Shift_JIS');                 // 8A (Shift_JISの先行バイト)
  test("A\x8A/", 'Shift_JIS');                // 8A / が続く
  test("A\x8A<", 'Shift_JIS');                // 8A < が続く
echo "-------------------------------\n";
  test("A\xB4", 'EUC-JP');                    // B4 (EUC-JPの先行バイト)
  test("A\xB4/", 'EUC-JP');                   // B4 に / が続く
  test("A\xB4<", 'EUC-JP');                   // B4 に < が続く

このスクリプトをPHP4.4.9(PHP4系の最終バージョン)、PHP5.2.0~PHP5.2.4で実行した結果は以下のようになります。バージョン表示以外は同じ結果です。

5.2.4
c0af:/ -> c0af:/
e080af:/ -> e080af:/
f08080af:/ -> f08080af:/
f8808080af:/ -> f8808080af:/
fc80808080af:/ -> fc80808080af:/
 -------------------------------
c0bc:< -> 266c743b:&lt;
e080bc:< -> 266c743b:&lt;
f08080bc:< -> 266c743b:&lt;
f8808080bc:< -> 266c743b:&lt;
fc80808080bc:< -> 266c743b:&lt;
 -------------------------------
41c2:A■ -> 41c200:A■ 
41c23c:A■< -> 41266c743b:A&lt;
41c22f:A■/ -> 41c22f:A■/
41e6bc:A■ -> 41e6bc00:A■ 
41e6bc3c:A■< -> 41266c743b:A&lt;
41e6bc2f:A■/ -> 41e6bc2f:A■/
 -------------------------------
418a:A■ -> 418a:A■
418a2f:A■/ -> 418a2f:A■/
418a3c:A■< -> 418a266c743b:A■&lt;
 -------------------------------
41b4:A■ -> 41b4:A■
41b42f:A■/ -> 41b42f:A■/
41b43c:A■< -> 41b4266c743b:A■&lt;

不正な文字エンコーディングは■で表示しています。UTF-8の冗長表現の場合でも記号がエスケープされているので、文字エンコーディング指定がまったく無視されているわけではありませんが、Shift_JISやEUC-JPに関しては、文字エンコーディングISO-8859-1が指定されているかのような動作です*1。ISO-8859-1はhtmlspecialcharsのマニュアルによれば、文字エンコーディング指定が省略された時のデフォルト値ですから、「htmlspecialcharsの第三パラメータは指定しても意味がない」と思う人が出ても不思議ではありません。続いて、PHP5.2.5~PHP5.2.11、PHP5.3.0の結果を示します(PHP5.2.7は欠番になったので試していません)。やはり、バージョンの表示以外は同じ結果です。

5.2.5
c0af:/ -> c0af:/
e080af:/ -> e080af:/
f08080af:/ -> f08080af:/
f8808080af:/ -> f8808080af:/
fc80808080af:/ -> fc80808080af:/
 -------------------------------
c0bc:< -> 266c743b:&lt;
e080bc:< -> 266c743b:&lt;
f08080bc:< -> 266c743b:&lt;
f8808080bc:< -> 266c743b:&lt;
fc80808080bc:< -> 266c743b:&lt;
 -------------------------------
41c2:A■ -> :
41c23c:A■< -> :
41c22f:A■/ -> :
41e6bc:A■ -> :
41e6bc3c:A■< -> :
41e6bc2f:A■/ -> :
 -------------------------------
418a:A■ -> :
418a2f:A■/ -> 418a2f:A■/
418a3c:A■< -> 418a266c743b:A■&lt;
 -------------------------------
41b4:A■ -> :
41b42f:A■/ -> 41b42f:A■/
41b43c:A■< -> 41b4266c743b:A■&lt;

確かに一部の■が取り除かれていますが、全てではありません。上記結果より、以下のような処理となっていることが伺えます。

  1. UTF-8の冗長表現は許容している
  2. UTF-8の冗長表現であっても、記号のエスケープは行われる
  3. UTF-8として不正な文字(冗長表現は別)が1カ所でもあれば、出力は空になる、
  4. Shift_JIS、EUC-JPの「半端な先行バイト」が入力の末尾にあると、出力は空になる
  5. Shift_JIS、EUC-JPの先行バイトに続くバイトが不正の場合、先行バイトは単独の文字として扱われる

個人的には、どうしてこんな中途半端な仕様にしたのだろうと思いますが、上記の仕様でXSSの攻撃に対する抜けが生じるかと言えば、突破の方法はちょっと思いつきません(上記第4項が防御に貢献しています)。しかしながら、半端な先行バイトやUTF-8の冗長表現を許容する点で危なっかしいことも確かですし、不正なシーケンスを出力する必要は全くないわけですから、以下のような仕様にすべきだと私は考えます。

  • 冗長なUTF-8は不正なエンコーディングとして扱う(出力を空にする)
  • Shift_JIS、EUC-JPの2バイト目が不正な場合も、エラーとして出力を空にする

htmlspecialcharsの変遷

htmlspecialcharsの仕様の変化を時系列で整理します。PHPのマニュアルによると、htmlspecialcharsの第三パラメータが追加されたのはPHP4.1.0(2001年12月)、そのパラメータが実際に機能しだしたのは、前述のようにPHP5.2.5(2007年11月)です。その他も含め、以下にhtmlspecialcharsの変遷をまとめました。

PHP3.0    1998/06/06    htmlspecialcharsの提供
PHP4.0.3  2000/10/11    第2パラメータ(quote_style)追加
PHP4.1.0  2001/12/10    第3パラメータ(charset)追加
                        UTF-8、Shift_JIS、EUC-JPもサポートされている
PHP5.2.5  2007/11/08    文字エンコーディングの検査が追加(完全ではない)

ごらんのように、htmlspecialcharsは、PHP3からサポートされている*2由緒正しい関数で、早い時期に文字エンコーディング指定ができるようになりました。しかし、文字コードのセキュリティという観点から言えば、当初の仕様では役にたたず、2007年11月のPHP5.2.5に至ってようやく最低限のチェックがなされるようなりました。

PHPプログラマの意識はどうか

次に、現場のPHPプログラマの意識について考察してみます。長い間charsetパラメータが有効でなかったため、「htmlspecialcharsのcharsetパラメータは指定しても無意味」という意識が、一部のPHPプログラマに定着しているのではないかと感じています。例えば、過去にはてなの日記「Shift_JISを利用することの是非」にて取り上げた「はじめてのPHPプログラミング 基本編5.3対応」という書籍には、以下のような記述があります。

(htmlspecialcharsは)通常は第2引数まで指定すればほぼ問題ありませんが、念には念を入れるのであれば第3引数(文字エンコーディング)まで指定した方が良いでしょう。
「はじめてのPHPプログラミング基本編5.3対応」P203より引用

これを読んだ読者は、第3引数は指定する必要はないのだなと思うことでしょう。また、同じエントリのコメント欄も興味深い内容です。

テストしましたがPHPに元々入っているhtmlspecialcharsに関してはSJISを第三引数に指定しても全く意味がないようです。

SJISとして認識できない半端文字の場合はSJIS指定しても貫通する様子なのですがどうでしょう。

[id:arrayさんのコメントより引用]

統計をとったわけではありませんが、この意見が現状のPHPプログラマの意識を象徴しているように感じます。ですが、現在のhtmlspecialcharsは不完全ながら文字エンコーディングをチェックしていますので、意識を変えていかなければいけませんね。

まとめ

htmlspecialcharsの第三パラメータにより文字エンコーディングを指定することにより、不正な文字エンコーディングによる攻撃をある程度防御していることが分かりました。しかし、前述のように中途半端な挙動であるため、入口でのバリデーションにより文字エンコーディングの妥当性チェックをしておくべきでしょう。また、PHP5.2.5以降を使用することが必須で、特別な理由がない限りPHPの最新バージョンを使うべきです。

また、今回のテーマに関連して、id:t_komuraさんの素晴らしいプレゼンテーションが公開されていますので、あわせて参考になさってください。私が指摘していない内容についても言及されています。

追記(2009/10/05)

id:t_komura さんがこの件に関して検証して下さいました。そのブログエントリ「Shift_JIS では、htmlspecialchars() を使用しても XSS が可能な場合がある」によると、
PHP の htmlspecialchars() では、SJIS(Shift_JIS) の場合、\xf0 - \xfc を単独で指定しても排除しない(PHP 5.3.0 で確認)

だそうです。

まぁ、入力・内部処理・出力を全てShift_JISで通す場合に問題になるものですので、通常このような文字エンコーディングの選択はしない方がよいとは思いますが、あり得ないことではないので注意が必要ですね。

文字コードの選択には、ITproに書いた解説をご参照ください。また、Shift_JISを通して使うことの問題については、はてなダイアリーに書いた「Shift_JISを利用することの是非」をご参照ください。

*1 UTF-8の際に不正な文字エンコーディングがあると末尾にナル文字がついていますが、これは単純なバグでしょうね。

*2 PHP/FIにはHtmlSpecialCharsという関数があったようですが


2009年09月24日 [SQLインジェクション][SQL]

_SQLの暗黙の型変換はワナがいっぱい

このエントリでは、SQLにおいて「暗黙の型変換」を使うべきでない理由として、具体的な「ワナ」をいくつか紹介します。 数値項目に対するSQLインジェクション対策のまとめにて説明したように、RDBの数値型の列に対してSQLインジェクション対策をする方法として、以下の三種類が知られています。

  1. バインド機構を用いる
  2. パラメータの数値としての妥当性確認を行う
  3. パラメータを文字列リテラルとしてエスケープする

このうち、方法3を使うべきでない説明の補足です。具体的には、方法3には、「暗黙の型変換」が発生しますが、それが思わぬ事故を招く可能性が高いことを説明したいと思います。
 方法3に従うと、最終的には以下のようなSQLを発行することになります。age列は年齢を示す列であり、整数型を想定しています。

SELECT * FROM sample1 WHERE age = '27'

挿入の場合は以下のようになります。

INSERT INTO sample1 (age) VALUES ('27')

これらに出てくる '27' という文字列リテラルは、SQL実行時に「暗黙に」型変換されます。

変換されるのは文字列型にか、数値型にか

暗黙の型変換では、プログラマは型の変換内容を明記しないため、RDB毎に持つ型変換ルールに従って型が決定されます。ISO/JISのSQLでは文字列型と数値型の「暗黙の型変換」は規定されていないため、データベースソフトウェア毎に処理系依存のルールがあります。

一番ありがちな以下のようなケースで考えてみましょう。数値型の列と文字列リテラルの比較です(ageは数値型の列)。

… WHERE age > '27'

左辺が数値型、右辺は文字列型です。この場合はどのように型変換されるのでしょうか。二種類の可能性があります。

  1. ageを文字列型に変換する
  2. '27'を数値型に変換する

どっちでも似たようなものと思われるかもしれませんが、この違いは重要です。処理結果とパフォーマンスに大きく影響するからです。結論としては、現在広く利用されているDBMS(MySQL、PostgreSQL、Oracle、MS SQL Server)では数値型に合わせられます。しかし、仮に文字列型にあわせるようなRDBMSがあったとすると、以下のような結果になります。

'9' > '27'

文字列の大小比較は辞書式順序に従うためです。

現実には、数値型にそろえる形で変換されるので、先のSQLは以下のように変形され、実行されます。

… WHERE age > 27

これはこれで大丈夫なのですが、こんどは列が文字列型、リテラルが数値型の場合で考えてみます。日本オラクル社にはかつて社員番号0を持つ社員犬「ウェンディ」がいたそうですので、彼女を検索してみましょう。

…WHERE employeeid = 0

社員番号を持つ列employeeidは、実際には文字列型であると想定します*1。この場合、列employeeidの方が数値型に変換されながら検索が実行されます。これはパフォーマンスの低下をもらたらします。インデックスが使用できないからです。実際問題として、数値型に変換後に 0 に一致する文字列は、「0」の他、「00」、「00000」など無数にあるため、そのようなインデックスは作成できないのです。結果として、文字列型の列と数値リテラルを比較すると、インデックスが使用されないためにパフォーマンスが低下します。

文字列型からどの数値型に変換されるのか

次に、文字列リテラルから数値型に「暗黙に」変換された結果は、どのような数値型になるのでしょうか。私が調べた範囲では、以下のような可能性があります。

  1. 浮動小数点数型になる
  2. 文脈に応じて柔軟に型が決定される

私が調べた範囲では、MySQLは常に 1.の浮動小数点数になり、PostgreSQL、MS SQL、Oracleは 2.の文脈に応じた型になるようです。あぁ、浮動小数点数と聞いただけでイヤな感じがしたあなた、あなたのイヤな感じは現実のものになります。以下、MySQL 5.1での実験結果を紹介します。まず、次のようなテーブルを用意します。

mysql> create table dtest0 (d0 decimal(20, 0));
Query OK, 0 rows affected (0.01 sec)

ご覧のように、十進20桁の列を一つ持つテーブルです。これに「文字列」を挿入してみます。

mysql> insert into dtest0 values('12345678901234567890');
Query OK, 1 row affected (0.00 sec)

mysql> select * from dtest0;
 +----------------------+
 | d0                   |
 +----------------------+
 | 12345678901234567890 |
 +----------------------+
1 row in set (0.00 sec)

挿入においては、期待通りに動いています。念のため、以下のSELECTでも確認してみましょう。

mysql> select * from dtest0 where d0 = 12345678901234567890;
 +----------------------+
 | d0                   |
 +----------------------+
 | 12345678901234567890 | 
 +----------------------+
1 row in set (0.00 sec)

問題ないですね。それでは、問題の「暗黙の型変換」を伴うパターンです。

mysql> select * from dtest0 where d0 = '12345678901234567890';
Empty set (0.00 sec)

あれ、見つからないですね。それでは、この列を挿入したらどうでしょうか。

mysql> insert into dtest0 values (12345678901234570000);
Query OK, 1 row affected (0.05 sec)

mysql> select * from dtest0 where d0 = '12345678901234567890';
 +-----------------------+
 | d0                    |
 +-----------------------+
 | 123456789012345670000 |
 +-----------------------+

今度は見つかりましたが、期待に反して、「12345678901234570000」という値がヒットしています。なぜでしょうか。
 実は、WHERE句の実行に先立ち、'12345678901234567890'が浮動小数点数に変換されているからです。その様子を実験してみましょう。

mysql> select '12345678901234567890'+0;
 +--------------------------+
 | '12345678901234567890'+0 |
 +--------------------------+
 |    1.23456789012346e+019 |
 +--------------------------+
1 row in set (0.00 sec)

0を加算することによって、数値への「暗黙の型変換」結果を表示させました。ご覧のように、浮動小数点数になっています。先の表示「12345678901234570000」とは末尾が少し異なりますが、これは丸めによるものです。このあたりが浮動小数点数のイヤラシイところですね。しかし、ここで示した挙動はMySQLのリファレンスマニュアルにちゃんと書いてあります。

次のルールは、比較の演算に対してどのように変換が行われるかを示しています :

  • 一方か両方の引数が NULL の場合、比較の結果は、NULL-safe <=> 等値比較演算子以外は、NULL になります。NULL <=> NULL の場合、結果は true です。

  • 【中略】

  • 他のすべてのケースでは、引数は浮動少数点 ( 実 ) 数として比較されます

[11.1.2. 式評価でのタイプ変換より引用]

ふつー、こんなとこまで読まないよと言いたくなりますが、「暗黙の型変換」を利用する場合は、このあたりのことも知った上で、SQLの実行結果を予測しなければならないということです。佐名木智貴氏が、「セキュアWebプログラミングTips集(ソフト・リサーチ・センター)」の中で述べられた名文句を思い出します。

ぜひ読者諸氏には今一度、自分の使っているデータベース・ソフトウェアのSQLリファレンスを通読することを推奨する(同書P213)。

私はとても「リファレンスを通読」する根性はないので、できるだけ「安全第一」のプログラミングにより、予期せぬ挙動が入らないようにしています。「暗黙の型変換を避ける」というのもその一つで、セキュリティ上の安全にもつながると信じます。

他のDBMSはどうか

ここで、他のDBMSにも少し触れます。PostgreSQL、MS SQL Server、Oracleは、調べた範囲ではMySQLほどイヤラシイことにはならなかったのですが、油断は禁物です。先に、MySQLで実行した「'12345678901234567890'+0」をPostgreSQL(8.4)で実行すると、以下のようになります。

test=# select '12345678901234567890' + 0;
ERROR:  値"12345678901234567890"は型integerの範囲外です
行 1: select '12345678901234567890' + 0;
             ^

以下は、MS SQL Server(SQL Server 2008)の実行結果

varchar の値 '12345678901234567890' の変換が int 型の列でオーバーフローしました。

これらPostgreSQLとMS SQL Serverでは、文字列リテラルと数値型の演算に際して、演算対象の数値型に型をあわせているようです。PostgreSQLのマニュアルには以下の記述があります。

文字列リテラルに型が指定されていない場合、後述するように、後の段階で解決されるようにとりあえず場所を確保するための型であるunknownが割り当てられます

[10.1. 概要より引用]

a. 二項演算子の1つの引数がunknown型であった場合、この検査のもう片方の引数と同一の型であると仮定します。

[10.2. 演算子より引用]

これを確かめるため、以下の実験をしてみました。PostgreSQL8.4の結果を示しますが、MS SQL Server 2008でも同様の結果です。

test=# select '12345678901234567890' + cast(0 as decimal(20,0));
       ?column?
 ---------------------
 12345678901234567890
(1 行)

数値0を decimal(20,0) という型にキャストしたので、文字列リテラルの方も同じ型にあわせてくれました。便利ですね…なんてことはないのであって、素直に以下のように書けばよいのです。

tokumaru=# select 12345678901234567890 + 0;
       ?column?
 ---------------------
 12345678901234567890
(1 行)

DB2はどうか

Oracleは割合融通の利くRDBMSであり、暗黙の型変換についても期待したとおりに動いてくれます。一方、IBM DB2は、SQL仕様を厳格に実装しているという印象があり、従来「暗黙の型変換」を許していませんでした。これは、寺田氏(id:teracc)が「DB2の文字列→数値変換」として検証結果をまとめておられます。そこでは、DB2のV9.5とV8.2にて、文字列から数値への暗黙の型変換がエラーになることが示されています。私も、DB2 Express-C 9.5にて追試をして、同様の結果を確認しました。

ところが、最近DB2 V9.7(試用版)にて同様の確認をしたところ、V9.7に至って暗黙の型変換が認められるようになっていました。以下は、V9.5での結果。

db2 => select '1'+1 from dual
SQL0402N  算術関数または演算 "+"
のオペランドのデータ・タイプが、数値ではありません。  SQLSTATE=42819

続いて、V9.7での結果です。

db2 => select '1'+1 from dual

1
 ------------------------------------------
                                         2

  1 レコードが選択されました。

db2 => select '12345678901234567890'+0 from dual

1
 ------------------------------------------
                      12345678901234567890

  1 レコードが選択されました。

まだ十分確認ができていないのですが、DB2 V9.7は、Oracleとの互換性をウリにしているため、暗黙の型変換もOracleの挙動に合わせたものではないかと想像しています。DB2のこの仕様変更は、暗黙の型変換の仕様がDBMS毎に異なるだけではなく、同一DBMSのバージョンによっても挙動が異なることを示す良い例だと考えています。

まとめ

SQLの「暗黙の型変換」による予期しない動作について紹介しました。暗黙の型変換の詳細仕様は実装依存であり、時として、利用者の想定以外の動作をする場合があり、危険です。暗黙の型変換を避け、型変換が必要な場合は、CASTによる明示的な変換を行うようにするべきです。予期しない動作の例として以下のようなものを説明しました。

  • 検索時にインデックスが使用できずにパフォーマンスが低下する
  • 暗黙の型変換により精度が損なわれる
  • 暗黙の型変換により、型の範囲外になるというエラーが発生する
  • DBのバージョンアップにより暗黙の型変換の仕様が変わる

というわけで、このエントリの結論は以下のようになります。

  • 例えば、暗黙の型変換を避ける
  • どうしても暗黙の型変換を使いたかったら、SQLリファレンスを通読してからにする(冗談です)

参考:

追記(2009/09/25)

IBM DB2の上記仕様変更について、id:umqさんから指摘を頂き、IBM DB2のリファレンスに以下の記述が追記されていることがわかりました。

ストリング・データ・タイプを使用するオペランドは、算術演算を実行する前に、CAST 指定の規則を使用して DECFLOAT(34) に変換されます。詳しくは、『データ・タイプ間のキャスト』を参照してください。このストリングには、数値の文字ストリング表記が含まれていなければなりません。

[DB2 Version 9.7 for Linux, UNIX, and Windowsより引用]

この記述は、Version9.5のリファレンスには存在せず、Version9.7で追加されたことが分かります。また、DECFLOAT(34)という型は、十進浮動小数点数で34桁の精度を持つという意味ですので、DECIMAL型の31桁、BIGINTの19桁をカバーする数値型として選ばれているようです。なんだかIBMらしい、富豪的な仕様ですね。

ともあれ、DB2 Version9.7から「暗黙の型変換」が正式にサポートされたことがはっきりしました。やはり、「暗黙の型変換」などというマイナーな仕様は、いつの間にか仕様が変更されることは十分あり得るわけで、そのようなあやふやなものに依存するプログラミングは危険だということだと思います。

*1 社員番号を文字列型で保持することは業務システムでは広く行われます


2009年09月18日 論点の整理

_文字エンコーディングバリデーションは自動化が望ましい

私が9月14日に書いたブログエントリPHP以外では - 既にあたり前になりつつある文字エンコーディングバリデーションに対して、大垣靖男さんから名指しで「セキュリティ専門家でも間違える!文字エンコーディング問題は難しいのか?」というエントリを頂戴しましたので、それに回答する内容を書きたいと思います。

まずは論点の整理から始めます。

合意していると思われる内容

 まずは合意できていると思われる内容から書き始めたいと思います。以下の内容は、大垣さんと私で合意事項だと考えています。

  • 論点1.文字エンコーディングの問題によるセキュリティ上の脅威がある
  • 論点2.文字エンコーディングに起因するセキュリティ上の問題に対して、文字エンコーディングのバリデーションが有効である
  • 論点3.Webアプリケーションによっては文字エンコーディングのバリデーションが不十分なことが原因で脆弱性が混入する場合がある

続いて、議論がかみ合ってないものの、実は合意していると思われる内容が論点4です。

  • 論点4.文字エンコーディング問題はバリデーションだけでは十分ではなく、総合的な対策が必要である

補足します。大垣さんの文章でいえば、例えば以下の部分です

しかし、中身を読めば私の意図はシステム全般に対して文字エンコーディングを厳格に処理しなければならない、と主張している事は分かるはずです

[何故かあたり前にならない文字エンコーディングバリデーションより引用]

私の方は、先のエントリでは触れていないだけで、やはり総合的な対策が必要だと考えています。その内容は、少し前にITproの連載で取り扱っています。

第5回から第9回が文字コード問題に関する総論、第10回から第12回がバリデーションに関する各論という位置づけです。実はこの8回でも終わりではなくて、表示(XSS対策)や、SQL呼び出し(SQLインジェクション対策)のところでも触れなければいけないと思いますが、ともかく文字コード問題を重視していること、バリデーションだけでは十分でないと私も考えていることは、お分かりいただけると思います。

とくに、第6回の文字集合に関する指摘は、ひょっとすると私のオリジナルの主張かもしれないと考えています。これは、2008年10月に開催されたセキュリティ・イベントBlack Hat Japan 2008におけるネットエージェント長谷川陽介氏*1の「趣味と実益の文字コード攻撃」というプレゼンテーション(資料はこちら)の中で「多対一の変換」として紹介されているものですが、文字エンコーディングの問題というよりは、文字集合の問題と整理した方が体系的に理解しやすいと思ったからです。

論点の整理について、話を戻します。元のエントリの冒頭には以下のように書きました。

大垣靖男さんの日記「何故かあたり前にならない文字エンコーディングバリデーション」に端を発して、入力データなどの文字エンコーディングの妥当性チェックをどう行うかが議論になっています。

つまり、先のエントリは大垣さんの主張全体に対して賛成とか反対とか言っているのではなく、大垣さんのエントリに刺激されて何人かのブロガーが意見を述べていることを受けて議論を展開しているものです。つまり、文字コード問題はバリデーションだけでは不十分であるという総論には同意しますが、でもバリデーションも大事だよねという各論の方に(私も含めて)何人かのブロガーが反応したわけですね。

意見が一致しない論点は自動か・手動か

さて、大垣さんと私で主張が一致していないかもしれないと思うのは、以下の論点です。

  • 論点5.文字エンコーディングの問題はアプリケーションプログラマができるだけ意識しないでも安全性が担保される仕組みが望ましい(徳丸の主張)

これは、元のエントリでは例えば以下の部分が該当します。

あるべき論で言えば、id:ikepyonが書いているように、言語などで自動的に検証が働くべきだと考えます。文字エンコーディングの妥当性は、文脈に関係なく、自動的にチェックできることですから、わざわざアプリケーションでチェックを記述する必要性はないからです。

そして、大垣さんは以下のように書かれていますので、アプリケーションが意識してチェックすべきとお考えなのだと認識しています。

RoRの脆弱性に関連してRuby1.9では安全、と解説されていますが、それはRuby1.9は不正な文字エンコーディングを受け付けないからです。個人的には明示的にバリデーションコードで検出する方が良いと考えています。

[何故かあたり前にならない文字エンコーディングバリデーションより引用]

また、大垣さんの最新のエントリでは以下のように述べられています。

このブログを読んでいる方の多くはWebアプリの開発者だと思うのでWebアプリ限定しますが、Webシステムの中心にいるWebアプリの開発者が「セキュリティ問題の責任はフレームワークやDBMSなどにある」と考えていては安全なWebアプリの開発が難しくなるばかりです。

すべての開発者が「セキュリティ対策を行う部分=自分が作っている部分」を"基本的な心得"として実践してれば、今よりもっと安全なシステム作れると思いませんか?

[セキュリティ対策を行うべき部分 - 自分が作っている部分より引用]

これはすなわち、アプリケーションプログラマがもっと自覚を持って、積極的にセキュリティ対策を行うべきだという主張と理解しました。その延長として、文字エンコーディングの問題も、アプリケーションプログラマが自覚・理解し、アプリケーション側での対策をとるべきだという主張だと理解しています。誤解があればご指摘下さい。

大垣さんの主張は総論としては賛成なのですが、しかしどこかで、アプリケーションと基盤ソフトウェアの責任境界に線を引く必要があります。それは、以下のように三つに分類できると思います。

  • (a)明らかにアプリケーションが責任をもつべき項目
  • (b)責任境界のあいまいなグレーゾーン
  • (c)明らかに基盤ソフトウェア(言語、ミドルウェア、OSなど)が責任をもつべき項目

文字エンコーディングのバリデーションに関して言えば、大垣さんの主張は(a)だと理解しています。徳丸の主張は(c)です。

仮に(c)という立場に立つと、「文字エンコーディングのバリデーションは言語が担当すべき内容なのにPHPは(デフォルト設定では)してくれない。これは問題ではないか」という主張につながります。

(a)という立場に立つと、「PHPは文字エンコーディングのバリデーションに利用できる関数がある*2なので便利だ。しかも、php.iniの設定次第では、パリデータをプログラミングしなくても安全な設定が可能だ。なんて便利なんだ」という主張になるかもしれません。

そして、異なる考え方が混在しているということは、現状は文字エンコーディングのバリデーションは、実態としては(b)のグレーゾーンなのです。

文字エンコーディングバリデーションの自動化の方向性

私は、今後のWeb開発環境の進化の方向性として、文字エンコーディングのバリデーションは基盤ソフトウェア側の担当になる *べき* だと考えています。そして、かつてはそうではなかったものの、最近では、文字エンコーディングのバリデーションを言語側で担当するように変化してきているととらえています。

Microsoftの場合、レガシーASPの場合は何もしてくれませんが、ASP.NETは、壊れた文字エンコーディングの問題を *起こす方が難しい* くらいです。そして、私はそれが言語の進化の正しい方向性だと考えています。ASP.NETにおける文字エンコーディングに関しては、こちらをご覧下さい。

Javaの場合も内部のUTF-16に変換するという過程を伴うので、壊れた文字エンコーディングによる問題を引き起こすのは難しいのですが、例外として過去のJavaVMにてUTF-8の冗長表現を許容するという問題がありました。その問題による脆弱性のサンプルはこちらをご覧ください。しかし、この問題にしても、脆弱性を再現するために *かなりわざとらしい* プログラミングをしなければなりませんでした。つまり、ブラックリストによる文字チェックをした後で文字エンコーディングを変換しています。「こんなことふつーしねーよ」と思いながら書いたことを思い出しますが、しかし、その種の「ふつーしねー」ようなことによりTomcatのディレクトリトラバーサル(CVE-2008-2938)が発生してる*3ので、絶対ないとは言い切れません。その意味で、2008年12月にリリースされたJava SE6 Update11にてUTF-8の冗長表現が禁止されたことで安全性が増しました。JavaVMの最新版を使う限り、「わざとらしい書き方をしても壊れた文字エンコーディングによる問題を起こすのは難しい」状態と考えています。

Ruby1.9の場合はいいですね。大垣さん自身が「Ruby1.9は不正な文字エンコーディングを受け付けない」と書かれています。

Perl5.8に関しては、誤解を与えてしまったようです。大垣さんが引用された私のサンプルは「万一壊れたUTF-8が入ってしまった場合」の実験です。通常の処理は、以下のように「入口でdecode、出口でencode」です。これらは文字エンコーディング変換とは限らなくて、外部文字エンコーディングがUTF-8の場合(すなわち文字エンコーディング変換をしない場合)でも、decode/encodeは必須です。

#!/usr/bin/perl
use strict;
use warnings;
use utf8;
use CGI;
use Encode 'decode', 'encode';

my $q = CGI->new;
my $p = decode('UTF-8', $q->param('p'));
my $ep = $q->escapeHTML($p);

print encode 'UTF-8', <<EOT;
Content-Type: text/html; charset=UTF-8

<html><body>
$ep
</body></html>
EOT

Perlに関して言えば、「壊れた文字エンコーディングによる問題を起こす方が難しい」とまでは言えません。decode/encodeを使わないで、言い換えればutf8フラグを立てないでプログラムを記述することも依然として可能だからです。しかし、decodeは文字エンコーディングのチェックや変換ではなく内部形式への変換という位置づけであること、Perl5.8以降の標準的な書き方としてはdecode/encodeが推奨されていることから、今後はこのような書き方が普及していくものと思われます。いずれにせよ無条件に安全になるわけではないかことから、私の先のエントリでは、以下のような前提を書かなければなりませんでした。

前提として、最新の処理系を使用すること、Perlに関しては、use utf8;によるモダンな書き方をすることは必須です。

これら四言語で文字エンコーディングのチェックを自動化できる背景には、id:rryu氏の指摘のように、これら言語はオブジェクト自体に文字エンコーディングの情報があるという事実も重要です。Javaと.NETの文字列は元々UTF-16固定ですし、Perlは5.8以降でutf8フラグが導入されました。Rubyは1.9にて文字列オブジェクト毎に文字エンコーディングを指定できるという凝った仕様になりました。一方、PHPの文字列には文字エンコーディングの情報は含まれないので、PHPのマルチバイト文字列を扱う関数には文字エンコーディングを指定するようになっています。

しかるに、PHPは、文字エンコーディングのバリデーション方法は複数用意されているものの、漫然とプログラムを書いたのでは文字エンコーディングはチェックされません。それは、大垣さん自身もブログなどで繰り返し指摘されていますし、技術評論社の「なぜPHPアプリにセキュリティホールが多いのか?」という連載の最新号「第27回 見過ごされているWebアプリケーションのバリデーションの欠陥」でも、「PHPに限った話ではない」という前提ではありますが、この話題を展開されている通りです。それに対して、私は、文字エンコーディングバリデーションは自動化が望ましいし、PHP以外の言語ではその方向に進化していると主張したかったわけです。これはあくまでも入口でのバリデーションに限った話です。バリデーションで対応できない場合の例として、ASP.NETとMS SQL Serverの組み合わせにより、文字集合の問題が原因でXSSが発生する可能性について、以前まっちゃ445目覚まし勉強会にてライトニングトークをいたしました。そのときの資料を公開しましたのでご覧下さい(スライド28以降です)。

文字エンコーディング問題は難しい。だからこそ自動化を

さて、大垣さんのブログタイトルの「セキュリティ専門家」はもちろん私のことを指しているのですが、いったんそれを忘れますと、「セキュリティ専門家でも間違える!文字エンコーディング問題は難しいのか?」という状況はまさにその通りだと思います。文字エンコーディングの問題は難しいと思います。先に触れたITproの連載を執筆するにあたり、私は、文字集合や文字エンコーディングの説明から始めなければならなかったのです。このことが、問題の難しさと、普及のなさを示しています。そして、そこで私が書いた内容をすべてのアプリケーションプログラマが理解しろというのも実は酷な話だと思います。アプリケーションプログラマが勉強すべき内容には、もっと優先度の高いものが他にあるだろうと思うからです

であれば、自動化できる部分はプラットフォーム側でどんどん自動化することが、進むべき正しい道だと思います。かつてC言語でアプリケーションを開発していたころは、プログラマ全員がバッファオーバーフローを気にしなければならなかったのに対して、JavaやPerlやPHPなどでアプリケーションを記述するようになった今、バッファオーバーフローを気にしなければならないエンジニアは限定できるようになりました。それと同じような状況が、文字エンコーディングのバリデーションについても起こって欲しいし、既にASP.NETなどではその望ましい状況になっていると私は考えます。

*1 氏の肩書きはイベント時の紹介に従っています

*2 言語によってはバリデーション専用の関数がないものも多い

*3 「ふつーしねーよ」という感想についてはここも参考になります


2009年09月14日 PHP以外では

_既にあたり前になりつつある文字エンコーディングバリデーション

大垣靖男さんの日記「何故かあたり前にならない文字エンコーディングバリデーション」に端を発して、入力データなどの文字エンコーディングの妥当性チェックをどう行うかが議論になっています。チェック自体が必要であることは皆さん同意のようですが、

  1. チェック担当はアプリケーションか、基盤ソフト(言語、フレームワークなど)か
  2. 入力・処理・出力のどこでチェックするのか

という点で、さまざまな意見が寄せられています。大垣さん自身は、アプリケーションが入力時点でチェックすべきと主張されています。これに対して、いや基盤ソフトでチェックすべきだとか、文字列を「使うとき」にチェックすべきだという意見が出ています。

たとえば、id:ikepyonの日記「[セキュリティ]何故かあたり前にならない文字エンコーディングバリデーション」では、このチェックは基盤ソフトウェアの仕事であって、アプリケーションでチェックするのはおかしいとしています。

一方、岩本隆史さんの日記「不正な文字列をどこでチェックすべきか」では、文字列を使う時(すなわち出力時)に文字エンコーディングの妥当性をチェックすべきだとしています。

ここで、そもそも、なぜ文字エンコーディングの妥当性確認が必要かという理由について考えてみると、

  • 不正な文字エンコーディングを利用したSQLインジェクションやXSSへの対策

というところから議論が始まっているようですが、いやいやそうじゃないだろうという突っ込みが入りそうです。より本質的な理由としては、以下のようなものが考えられます。

  1. 入力データがUTF-8(あるいは他の文字エンコーディング)であるというアプリケーション要件に対するチェック
  2. 内部データがUTF-8(あるいはUTF-16など)として妥当でないと、処理系が誤動作を起こすため

という理由が考えられます。1.に関連して、id:kazuhooku氏がはてなブックマークにて「UTF-8 を入れるべきフィールドに壊れた UTF-8 が渡されてきたら、そもそも受け付けるべきじゃない。8桁の郵便番号が入力されたら弾くのと同じ話」とコメントされていて、この意見に私も同意です。次に、2.について、現実の処理系として、Perl、Java、ASP.NET、Ruby、PHPについて、この問題の現状を復習してみます。

Perlの場合

大垣さんが参照されているところの、小飼弾さんの日記「perl - EncodeでXSSを防ぐ*1」をはじめ、小飼さんが繰り返し説明されているように、「入り口で decode して、内部ではすべて flagged utf8 で扱い、出口で encode する」という原則に従うことより、自動的に文字エンコーディングに対するバリデーション*2が働きます。これは、ファイル入出力などの場合も同様です。

一方、なんらかの理由で冗長なUTF-8となるバイト列がUTF-8フラグつきで入ってしまった場合でも、文字列比較などでは、最短形式に正規化した上で処理がなされている模様です。これについて、以下のコードで少し具体的に説明します。以下のコード(Perl5.8.8とPerl5.10.0で検証)について

$ cat uni01.pl
#!/usr/bin/perl
use strict;
use utf8;
use Encode 'encode';
# 非最短形式の'/'を強引に作成
my $s = pack("U0C*", 0xC0, 0xAF);
printf "is_utf8=%d\n", utf8::is_utf8($s);
print encode('utf8', "$s\n");
print $s eq '/' ? "eq\n" : "ne\n";
print $s =~ m#/# ? "matched\n" : "not matched\n";

pack("U0C*" ...) については、ここなどを参照下さい。UTF-8の内部形式をバイト列指定で強引に作成しています。このスクリプトの結果を予想できますか?

$ ./uni01.pl
is_utf8=1
/
eq
not matched

表示上は「/」となっていますが、ダンプするとこれは0xC0 0xAFがそのまま出ています。まぁ、エラーにしないのであれば、そうするしかないでしょうね。

注目すべきは、スラッシュとの比較と、正規表現でのマッチの結果です。比較においては「/」と同一、検索においては「/」にマッチしないという結果になっています。一貫性がないようにも見えますが、どうなるのが *正しい* 結果なのでしょうか。

比較において、(最短形式の)「/」と同一となるという結果は、いわば「安全サイド」の判断だと思います。こうしておけば、UTF-8の非最短形式の攻撃に対して、防御できる可能性があるからです。一方、正規表現の方はそうではなくて、たとえばディレクトリ・トラバーサル対策として、「/」や「.」をチェックしようとしても、このチェックをくぐり抜けてしまうことになります。

もっとも、Perlの場合、外部から取り込んだ文字列は全てdecodeするルールですので、その際に非最短形式のバイト列はU+FFFDに変換されるか、例外が発生するので、いずれにせよ、Perl5.8で導入されたutf8の仕組みをルール通り使っている限りは問題はおきません。詳しくは先ほど参照したこちらを参照下さい。

ここまでの内容を、入力・処理・出力の時点ごとに下表にまとめました。

入力時decode時にバリデーション
処理時何もしない。例外的に、比較処理などで正規化してから処理
出力時何もしない

Rubyの場合

Rubyの場合は、Ruby1.9にて文字エンコーディングの取り扱いが大幅に改善され、RubyでCGIプログラムを記述した場合早期に文字エンコーディングの検証が入るようになっています。たとえば、以下のような、クエリ文字列を受け取って表示するだけの簡単なプログラム。

#!/usr/local/bin/ruby
# -*- coding: utf-8 -*-
require "cgi"
cgi = CGI.new
is = cgi["p"]
ie = CGI.escapeHTML(is)
print "Content-Type: text/html; charset=UTF-8\n\n"
print "<html><body>"
print "inputdata = #{ie}</body></html>"

これに、%C0%AF(冗長な「/」)を指定すると、以下のようにCGI::InvalidEncoding例外が発生します。

$ ./test2.rb
(offline mode: enter name=value pairs on standard input)
p=%C0%AF
/usr/local/lib/ruby/1.9.1/cgi/core.rb:602:in `block (2 levels) in initialize_query': Accept-Charset encoding error (CGI::InvalidEncoding)
        from /usr/local/lib/ruby/1.9.1/cgi/core.rb:597:in `each'
        from /usr/local/lib/ruby/1.9.1/cgi/core.rb:597:in `block in initialize_query'
        from /usr/local/lib/ruby/1.9.1/cgi/core.rb:596:in `each'
        from /usr/local/lib/ruby/1.9.1/cgi/core.rb:596:in `initialize_query'
        from /usr/local/lib/ruby/1.9.1/cgi/core.rb:754:in `initialize'
        from ./test2.rb:3:in `new'
        from ./test2.rb:3:in `
' $

ブラウザから実行すると、500エラーになります。まぁ500エラーはあんまりなので、例外処理で捕捉することになりますが、ともかく文字エンコーディングの不正なバイト列による攻撃はできないことになります。

Webアプリケーションの入口でバリデーションが入ることは確認できましたが、無理矢理UTF-8の非最短形式を変数にセットしたらどうなるか、Perl同様のテストをしてみました。

#!/usr/local/bin/ruby
# -*- coding: utf-8 -*-
s = "\xc0\xaf"
puts s.encoding
puts s
puts s == '/' ? '==' : '!='
puts /\// =~ s ? 'matched' : 'not matched'

このスクリプトの実行結果は以下のようになります。

UTF-8
/
!=
./test2.rb:7:in `
': invalid byte sequence in UTF-8 (ArgumentError)

Perlの時とは異なり、==による比較では「等しくない」という結果で、正規表現によるマッチングでは例外が発生しています。この仕様は実用的なように思えます。セキュリティ対策のバリデーションやらエスケープの際には正規表現が多用されるので、そこで文字エンコーディングのチェックが入っていれば、セーフティネットとしての働きが期待できそうです。ともかく、RubyによるWebアプリケーション開発では、入口での文字エンコーディングチェックが働いていることになります。

Perl同様のまとめを行うと、下表のようになります。

入力時CGIクラスにてパラメタ取得時にバリデーション
処理時何もしない。例外的に正規表現にて例外発生
出力時何もしない

Javaと.NET

Javaと.NETの場合は、プログラム内部ではUTF-16が利用されており、外部から取り込んだ文字列は文字エンコーディングの変換が行われます。この過程で、文字エンコーディングの不正なバイト列はチェックされます。このあたりは以前ITproに書いたので、ここ(Javaの場合)ここ(ASP.NETの場合)を参照下さい。どちらも不正なバイト列はU+FFFD(Replacement Character)に変換されます。

U+FFFDをアプリケーション側でチェックしてエラーにした方がよいと思いますが、仮にエラーにしなくても、U+FFFDを悪用した攻撃は知られていないので、不正なバイト列を用いた攻撃は成立しないと考えられます。つまり、Javaと.NETは文字エンコーディングの不正なバイト列に関しては安全と言えます*3

PHPの場合

PHPの場合、内部処理に用いられる文字エンコーディングは特に規定されていないので、どんなバイト列でも扱うことができます。たとえば、UTF-8で処理している場合でも、UTF-8の非最短形式のデータも、変数に保持することは可能です。一方、文字列比較や正規表現でのマッチングにおいては、暗黙のうちにUTF-8の最短形式であることを要求しているため、非最短形式(冗長形式)のデータをうまく扱うことができません。これにより、ディレクトリ・トラバーサル対策として「..」や「/」をpreg_matchやmb_eregでチェックしていても、冗長形式のバイト列がマッチしないため、チェックをすり抜けることになります。

現実にUTF-8非最短形式のデータが悪さをするには、通常は別の文字エンコーディングに変換された後なので、mb_convert_encodingなどで文字エンコーディング変換した際に、不正なバイト列が捕捉されると考えられます。それでも、本来のチェックロジックをすり抜けて、「後でどこかで引っかかる」ことに期待するのは気持ち悪いし、安全でもないと考えられます。

従って、PHPを使う場合、データを入力した時点(ファイルなどからも含む)で文字エンコーディングの妥当性確認をするくらいしか、文字エンコーディングの問題に対する現実的な対策はないと考えます。

そもそも、どうあるべきか

ここまで見てきたように、PHP以外の言語(Perl、Java、.NET、Ruby)の最新版では、文字エンコーディングの不正をチェックする機構が組み込まれていて、これら言語の通常の使用の中で(プログラマが意識することなく)そのチェックが働くことが分かります。一方、PHPにおいては、このようなチェック機構は言語組み込みではなく、mb_check_encodingなどをアプリケーション(あるいはフレームワーク)側で呼び出してチェックしてやる必要があります。

あるべき論で言えば、id:ikepyonが書いているように、言語などで自動的に検証が働くべきだと考えます。文字エンコーディングの妥当性は、文脈に関係なく、自動的にチェックできることですから、わざわざアプリケーションでチェックを記述する必要性はないからです。

我々はどうすべきか

文字エンコーディングの問題だけに注目すれば、PHPを避けるのも一つの考え方かもしれません。すなわち、Perl、Java、.NET、Rubyについて言えば、この問題は既に「解決済み」の問題であり、これら文字エンコーディングに関して安全な言語を選択するという考え方です。前提として、最新の処理系を使用すること、Perlに関しては、use utf8;によるモダンな書き方をすることは必須です。

PHPに関しては、PHP6で内部の文字エンコーディングがUTF-16になり、IBMのInternational Components for Unicode(ICU)が組み込まれるとのことですので、PHP6に至ってようやくJavaやPerl並になると予想されます。しかし、当面PHP6が出てくる気配はなさそうですので、PHPプログラマがとれる道は次の二つだと考えます。

  1. PHPをあきらめ、RubyやPerlを使う
  2. プログラムの先頭で文字エンコーディングの妥当性をチェックできるフレームワークやライブラリを利用する

大垣さんは、おそらく後者を主張されているのでしょう。過去に、大垣さんは「なぜPHPアプリにセキュリティホールが多いのか?/第27回 見過ごされているWebアプリケーションのバリデーションの欠陥」の中で、以下のようなバリデーションプログラムを例示されています

<?php
function _validate_encoding($val, $key) {
    if (!mb_check_encoding($key) || !mb_check_encoding($val)) {
        trigger_error('Invalid charactor encoding detected.');
        exit;
    }
}
$vars = array($_GET, $_POST, $_COOKIE, $_SERVER, $_REQUEST);
array_walk_recursive($vars, '_validate_encoding');

ごらんのように、全てのGETとPOSTの変数、サーバー変数などを十把ひとからげにチェックするものです*4。便利そうですが、$_SERVERまでまとめて文字エンコーディングのチェックをしていいのかという疑問が出ますし、大垣さん自身の以下のコメントと矛盾しそうな気がします。

RoRの脆弱性に関連してRuby1.9では安全、と解説されていますが、それはRuby1.9は不正な文字エンコーディングを受け付けないからです。個人的には明示的にバリデーションコードで検出する方が良いと考えています。クライアントからの入力は文字列のみでなくイメージ等のバイナリも含まれるからです

[何故かあたり前にならない文字エンコーディングバリデーションより引用]

私なら、もう少し明示的に、以下のようなライブラリ関数を用意するか、同等の機能をフレームワークに組み込む、あるいは類似機能を持つフレームワークを選択するでしょう。

// 文字エンコーディング、文字種、文字数のチェック
function checkValue($val, $pattern) {
  if (! mb_check_encoding($val, 'UTF-8')) {
    trigger_error('文字エンコーディングの不正');
    exit;
  }
  // 文字エンコーディングの変換(必要があれば
  // $val = mb_convert_encoding($val, 'UTF-8', 'UTF-8');
  if (! mb_ereg($pattern, $val)) {
    trigger_error('文字種または文字数が範囲外です');
    exit;
  }
  return $val;
}
// 文字エンコーディング、文字種などのチェックつきパラメタ取得
function getGETvalue($key, $pattern) {
  return checkValue($_GET[$key], $pattern);
}
//呼び出し例(制御文字以外で10文字以下)
$p = getGETvalue('p', "\\A[[:^cntrl:]]{0,10}\\z");

ご覧のように、文字エンコーディングだけでなく、文字種と文字数のチェックも同時に行っています。文字種のバリデーションも同時に行える「便利な関数」を提供することにより、文字エンコーディングの問題をプログラマに意識させなくても自動的にチェックできるようにしています。これくらいのものを用意しないと、アプリケーションでの文字エンコーディングチェックを定着させるのは難しいでしょう。上記で出てきた正規表現などの説明はこちらを参考にしてください。

まとめ

文字エンコーディングの妥当性チェックについて、現在Webアプリケーション開発によく用いられるPerl(5.8以上)、Ruby1.9、Java、ASP.NET、PHP5.Xについて現状と、あるべき使い方について検討しました。

全ての言語において、変数に文字エンコーディングとして異常な値が入ると、その後の処理(文字列比較、正規表現による検索など)が異常になる場合があります。その意味で、入り口(データを変数に格納した時点)での文字エンコーディングの妥当性確認は必須です。別の観点から言えば、アプリケーション要件として(セキュリティとは関係なく)文字エンコーディングの妥当性確認をする必要があります。

前述のように、PHPを除く言語(プラットフォーム)では、既に文字エンコーディングのバリデーションは言語自体で行われることが当たり前になっており、アプリケーションプログラマが意識しなくても、自然な形でバリデーションが行われます。一方、PHPにはそのような機能がまだ組み込まれておらず、当面ライブラリやフレーワークの工夫により文字エンコーディングのチェックを確実にする必要があることを指摘しました。

以上の議論から、大垣さんのブログタイトルは、以下のようにされた方がより正確でありましょう。

何故か *PHPでは* あたり前にならない文字エンコーディングバリデーション

続きはこちら→「文字エンコーディングバリデーションは自動化が望ましい

*1 私のITpro連載に対する補足を頂いた内容となっています

*2 U+FFFD(Replacement Character)への変換も含めます

*3 先に書いたように、Java SE6 Update 10までのJava実行環境は、UTF-8の非最短形式を許容していたため、アプリケーションの記述方法によってはこの影響を受ける場合がありました。Java SE6 Update 11以降であれば問題ありません

*4 $_REQUESTは、PHPマニュアルによると、「$_GET、 $_POST そして $_COOKIE の内容をまとめた連想配列です」

本日のツッコミ(全12件) [ツッコミを入れる]

Before...

_ 通りすがり [私が説明不足だったのは本当だ。私の責任。「PHP以外では」問題と「主張ずれ」問題(と「内部エンコードなし」問題)をす..]

_ otsune [>苦言?感想?この記事を読んだ人が誤解しないようにするために書いたのだから まぁ私の目的は「喧嘩売られてる? それ..]

_ 通りすがり [私の主張を理解して頂いたようで嬉しいです。]


2009年08月05日 i-mode2.0セキュリティの検討

_携帯JavaScriptとXSSの組み合わせによる「かんたんログイン」なりすましの可能性

このエントリでは、携帯電話のブラウザに搭載されたJavaScriptと、WebサイトのXSSの組み合わせにより、いわゆる「かんたんログイン」に対する不正ログインの可能性について検討する。

5月28にはてなダイアリーに書いた日記「i-mode2.0は前途多難」にて、今年のNTTドコモの夏モデルP-07AにてJavaScript機能が利用停止されたことを指摘した。同日付のNTTドコモ社のリリースによると、「ソフトウェア更新に伴い、高度化した機能の一部をご利用いただけなくなっていますが、再びご利用いただけるよう速やかに対処いたします」とあったが、それ以来2ヶ月以上が経つものの、未だにJavaScript機能は利用できない状態のままだ。

実は、NTTドコモ社が慌てふためいてJavaScript機能を急遽停止した頃から、私の頭の中には一つの仮説があったのだが、JavaScript機能が再開されてから確認しようと思ってそのままにしていた。しかし、中々JavaScript機能が再開されないことと、早期にサイト開発者に注意を呼びかけて予防的な対応をしてもらった方がよいと考えたことから、ここにその仮説を公開する。

その仮説とは、タイトルに記した通り、携帯電話のJavaScriptを悪用して「かんたんログイン」に対してなりすましが可能かどうかというものだが、攻撃が成立するための条件をまとめると以下のようになる。

  1. 携帯電話のJavaScriptでXMLHttpRequestオブジェクトが利用できる
  2. XMLHttpRequestにてsetRequestHeaderメソッドが利用できる
  3. setRequestHeaderメソッドにてUserAgentなどのリクエストヘッダが書き換えできる

iモード2.0の仕様書によると、上記の(1)と(2)を満足している。(3)については何も書いていないが、特にできないとも書いていないので、iモード2.0で上記が可能かもしれない。JavaScript機能が停止されていることから、現時点では確認する手段がない。

以下、具体的に説明する。

iモードの場合

iモードの「かんたんログイン」の場合、iモードIDか、FOMA端末製造番号あるいはFOMAカード製造番号(UIM)を使う。iモードIDは、拡張リクエストヘッダ「X-DCMGUID」に、FOMA端末製造番号およびFOMAカード製造番号はUserAgentに付与される。

したがって、UserAgentあるいはX-DCMGUIDをsetRequestHeaderメソッドにて書き換えれば、任意ユーザになりすましが可能になる。これが可能になる条件としては、攻撃対象となるWebサイトにクロスサイトスクリプティング(XSS)脆弱性がある必要がある。他のサイトからでは、XMLHttpRequestのSame Origin Policyの制限のため送信できない*1

すなわち、iモードの「かんたんログイン」を突破するのに必要な条件は以下のようになる。

  • setRequestHeaderにてUserAgentあるいはX-DCMGUIDを書き換え可能
  • 攻撃対象サイトにXSS脆弱性がある

前者に関しては、現在iモード2.0のJavaScript機能が停止されているので確認できない。X-DCMGUIDに関しては、おそらくドコモのゲートウェイで付与していると思われるので、ゲートウェイ側で削除するかもしれない。SSL通信の場合は、HTTPリクエストの内容をゲートウェイにて追加・変更・削除ができないが、元々X-DCMGUIDはSSLで受け取れないので、アプリケーションがSSLでもX-DCMGUIDを受け付けるか否かは、サイト側の実装に依存する。setRequestHeaderによるUserAgentの書き換えは、Firefoxでは可能なので、iモード2.0でも可能かもしれない。この場合は、後述のように、他事業者(au、ソフトバンク等)のユーザへのなりすましも可能になる場合がある。

後者に関しては、京セラコミュニケーションシステムが発行している「2009年版 Webアプリケーション脆弱性傾向」によると、携帯電話向け44サイトを含む177サイトを検査した結果、67%のサイトにXSS脆弱性がある。残念ながら携帯向けサイトだけの比率は公表されていないが、おそらく携帯電話向けサイトでも多くの割合でXSS脆弱性が検出されていると思われる。

au(KDDI)ユーザへのなりすまし

iモード2.0の端末を使って、auのユーザになりすましができる可能性がある。その条件は以下のようなものだ。

  • 携帯事業者(auであること)の判定をリクエストヘッダ(UserAgentまたはX-UP-SUBNO)のみで行っている
  • setRequestHeaderにてリクエストヘッダX-UP-SUBNOが追加できる
  • 事業者判定をUserAgentで行っているサイトの場合は、setRequestHeaderにてUserAgentが書き換え可能
  • 攻撃対象サイトにXSS脆弱性がある

ここで、一番目の条件について説明する。携帯事業者の判定方法には、一般に、

  1. IPアドレスを使用する方法
  2. UserAgentを使う方法
  3. 両者を併用する方法

の三種がある。かんたんログインの実現には、PCからのなりすまし防止のため、IPアドレスの帯域チェックは不可欠だが、IPアドレスのチェックを事業者の判定まではせず、単に「携帯電話からのリクエスト」であることのみを確認する場合があるのだ。

IPアドレス制限を実装する方法としては、

  1. ファイアウォールで制限する
  2. Webサーバの機能で制限する(Apacheの場合はhttpd.confや.htaccessに記述)
  3. アプリケーションで判定して制限する

の三通りがある。この中で、PC(携帯電話でない端末)からの攻撃を確実に防ぐという点では(i)がもっとも優れており、以下(ii)、(iii)の順になる。(iii)の場合だと、Webサーバ(Apache等)やアプリケーションサーバ(Tomcat、PHP等)に対する攻撃は止められないからだ。一方、(iii)を使用している場合は、IPアドレス帯域から事業者を判定することは容易だ。

以前紹介した書籍「PHP×携帯サイト 実践アプリケーション集」の場合は、まずUserAgentから事業者を判定しておいて、IPアドレスチェックの際に、当該の事業者のIP帯域に含まれているかを確認しているので、上記分類でいえば(c)および(iii)の組み合わせに相当する。一方、最近公開された実際に動いてすぐ使える「PHPによるかんたんログインサンプル」を作ってみました*2では、IPアドレスの制限は.htaccessで行うように指示しており、IPアドレスによる事業者のチェックはしていないので、同じく(b)および(ii)の組み合わせに相当する。

話を戻すと、方法(b)を用いた場合、携帯電話によるなりすまし行為が可能になった場合、若干脆弱になる。つまり、携帯電話JavaScriptのsetRequestHeaderメソッドにより、他事業者へのなりすましの可能性がある。すなわち、ドコモの携帯電話からの攻撃なので、IPアドレス帯域は携帯事業者のものである。UserAgentの書き換えが行われているので、アプリケーションはau携帯からのリクエストと誤認する。そこで、JavaScriptによるリクエストヘッダX-UP-SUBNO(EZ番号)追加により、別ユーザになりすましができるというシナリオである。

ソフトバンクの携帯電話ユーザへのなりすまし

ソフトバンク・ユーザに対するなりすましも、auの場合とおおむね同じだ。ソフトバンクの場合、個体識別番号が二種類ある。端末シリアル番号(UserAgentに付与)とユーザID(リクエストヘッダX-JPHONE-UID)である。そこで、なりすまし可能となる条件は以下の通りだ。

  • 携帯事業者(ソフトバンクであること)の判定をリクエストヘッダ(UserAgentなど)のみで行っている
  • setRequestHeaderにてUserAgentが書き換え可能
  • setRequestHeaderにてX-JPHONE-UIDが書き換え可能(ユーザIDで認証している場合)
  • 攻撃対象サイトにXSS脆弱性がある

NTTドコモへの要望

携帯JavaScriptによる、かんたんログインへのなりすまし攻撃の可能性について検討した。前述のように、現在iモード2.0端末のJavaScript機能は停止されており、上記条件を確認することはできない。おそらく、NTTドコモ社および端末メーカー、ブラウザメーカーは上記のような懸念も含めて対策をしておられる最中なのだろうと想像する。

本稿の検討により、携帯電話のJavaScriptでは、setRequestHeaderメソッドに一定の制限を設ける必要があることがわかる。

  • UserAgentを書き換えできないようにする
  • X-DCMGUID、X-UP-SUBNO、X-JPHONE-UIDなど、他事業者のものを含め、個体識別番号の指定に用いられるリクエストヘッダの指定もできないようにする

とくに、UserAgentの書き換え防止は重要である。さらに安全を期するためには、setRequestHeaderには大幅な制限を設けるのがよく、安全な仕様の例としては、追加・書き換え可能なリクエストヘッダをホワイトリストとして指定することなどが考えられる。

Webアプリ開発者側の対策

ここまで説明したように、かんたんログインに対するJavaScriptによるなりすましでは、いずれのパターンでもXSS脆弱性を悪用している。従って、XSS対策を行うことが根本的な対策になる。携帯電話向けだからと言って手を抜かないことが重要だ

また、保険的対策として、以下を推奨する。

  • かんたんログイン以外の認証手段を用意しておく
  • 重要な処理の前でパスワードなどによる再認証を求める
  • 携帯電話事業者の判定にIPアドレスを利用する

さらにいえば、究極の根本対策として「かんたんログインを使用しない」ことも検討いただきたい。

まとめ

携帯電話のJavaScriptとXSS脆弱性の組み合わせにより、かんたんログインに対するなりすましの可能性について報告した。

通常のXSS脆弱性に対する攻撃が、正規ユーザを媒介とした受動的攻撃であるのに対して、かんたんログインのなりすましは能動的な攻撃となる*3。それだけ、被害の規模も大きくなることが想定される。

かんたんログインに対する脅威はJavaScriptばかりではない。hideden氏の8月1日付けの日記「SoftBank Mobileの携帯用GatewayをPCで通る方法のメモ」で示されたような、携帯電話をモデムとして使用して事業者のゲートウェイ経由でのアクセスが可能となる場合にも、同様のなりすましの懸念がある。参照先ではソフトバンクモバイル内でのなりすましのみ指摘されているが、本稿で示したUserAgentの書き換えを併用することにより、他事業者(ドコモ、auなど)ユーザへのなりすましも懸念される。こちらの方法は、攻撃自体はPCからできるので、いっそう深刻な問題と言えるかもしれない。

そして、おそらくこの種の脅威は今後も形を変えて現れるだろう。その理由は、かんたんログインという手法が、もともと暗号学等の理論的な根拠に裏付けられておらず、携帯電話網という閉じた世界でのみ通用する手法であるからだ。今後、携帯電話そのものの高機能化や、携帯電話網への多様な端末の投入により、その前提が覆る可能性は十分あるし、その兆候は既に現れてる。

とすれば、少なくとも「いつでもかんたんログインを捨てられる」状態にしておくことが、ユーザも、Webサイト運営者自身をも守る最低限の取り組みであると、私は考える。

最後に

若干の宣伝をさせてください。筆者の経営するHASHコンサルティング株式会社では、Webアプリケーションの安全性に関するコンサルティングや脆弱性検査サービスなどを提供しています。携帯電話向けサイトの脆弱性対策については10年ほどの経験があります(前職での経験を含みます)。ご相談はこちらからお気軽に。

HASHコンサルティングは非常に小さな会社ですので、与信などの点で心配される向きもあろうかと思います。その場合は、私が技術顧問をしている 京セラコミュニケーションシステム株式会社にご相談いただければと思います。問い合わせフォームから「技術顧問の徳丸に相談したい」と書き添えていただければ確実かと思います。

様々な形で、貴社のWebサイトの安全にお役に立つことができれば幸いです。

2009/10/26追記

ようやく10月末からiモードブラウザ2.0のJavaScript機能が再公開されると発表がありました。これに合わせて、KCCSにて11月12日(木曜)、11月19日(木曜)、12月10日(木曜)の3回にわたり、ケータイWebセキュリティのセミナーを実施することになりましたので、興味のある方は、こちらのリンクから内容確認の上お申し込みください。

*1 これに関しては実機での確認が間に合った

*2 このエントリは私が見たかんたんログインの解説としてはもっとも行き届いていると思う

*3 これをXSSと呼んでよいかどうかは議論があるだろうが、攻撃形態により脆弱性の呼び名を変えるのも煩わしいこと、XSSが既に定着した名称であることから、本稿ではXSSという呼び方で統一した

本日のツッコミ(全8件) [ツッコミを入れる]

Before...

_ 徳丸浩 [matsuiさん、コメントありがとうございます。どうも、前提の違いがあるようですね。 私のこのエントリでは、攻撃は..]

_ 徳丸浩 [hidedenさん、大変詳しいレポートをありがとうございます。さすがに色々制約はあるのですね。 ですが、少なくとも「..]

_ ketaiorg [徳丸浩さん なるほどおっしゃるとおりです。 いずれにしてもこの方法は効果が薄いですね。大変参考になりました。 度重な..]


2009年03月28日

_IPAは脆弱性の呼び方を統一して欲しい

昨日の日記で、安全なウェブサイトの作り方セキュア・プログラミング講座とでは使っている用語が異なることを指摘した。
違いを理解いただくために、簡単に一覧表にまとめてみた。

安全なウェブサイトの作り方 セキュア・プログラミング講座
SQL インジェクション SQL注入
OS コマンド・インジェクション コマンド注入攻撃
パス名パラメータの未チェック/ディレクトリ・トラバーサル ディレクトリトラバーサル攻撃
セッションIDの固定化 セッションIDお膳立て
クロスサイト・スクリプティング スクリプト注入(XSS)
CSRF(クロスサイト・リクエスト・フォージェリ) リクエスト強要(CSRF)
HTTPレスポンス分割とキャッシュ汚染 HTTPレスポンスによるキャッシュ偽造
メールの第三者中継 メールの第三者中継
アクセス制御や認可制御の欠落 ユーザ認証(本人認証)とアクセス認可

黄色に塗ったところが用語が異なる部分だ。安全なウェブサイトの作り方が概ね世間で使用されている用語に従っているのに対して、セキュア・プログラミング講座の方は独自用語を用いる傾向にある。私見としては、この種の用語はむやみに発明せずに、一般に使用されている用語に従うのがよいと考えているが、せめてIPAで発刊するコンテンツの中では用語を統一していだきたい。技術的に正確な議論をするには、まず正しい用語を使うことが出発点であるし、IPAには正しい用語の統一に関して、指導的な立場を期待されていると考える。

本日のツッコミ(全2件) [ツッコミを入れる]

_ とおりすがり [IPAが公募で丸投げしているからなんじゃないでしょうか? 作ったところが違うから用語も違うと。 IPAが用語辞書を作..]

_ 徳丸 浩 [とおりすがりさん、コメントありがとうございます。 確かに、この旧版はセントラルコンピュータサービスが委託されています..]


2009年03月27日

_IPAの新版「セキュア・プログラミング講座」がイマイチだ

2002年3月にIPAから公開されたセキュア・プログラミング講座は、その後2007年に新版が公開された。旧版は今から7年前のコンテンツがベースになっているので現在の目から見ると色々突っ込みどころがあるが、2002年という時期にこれだけのコンテンツを揃えたというのは立派な仕事だったと考えている。

一方、2007年の新版はどうか。部分的に見れば「開発工程と脆弱性対策」の関連について言及するなど意欲的な内容もあるが、全体としては物足りない。新版が出た当時も「例えば、PHPを避ける」という表現が話題になったくらいで、同じIPAから公開されている「安全なウェブサイトの作り方」に比べれば、あまり影響力がないように思える。

しかし、「安全なウェブサイトの作り方」の第一版が2006年1月に出ているのであるから、その後に「セキュア・プログラミング講座」の新版を公開するからには、「安全なウェブサイトの作り方」の内容を包含し、その詳細版という位置づけであって欲しいと思うのだが、そのような内容にはなっていないのだ。大は、脆弱性に対する考え方から、小は、脆弱性の呼び方に至るまで、まったく異なるコンテンツになっている。これでは、両方のコンテンツを利用するユーザは混乱するだろう。

SQLインジェクション対策はどうか

これ以降は、「セキュア・プログラミング講座」の中から、現在緊急の課題であるSQLインジェクション対策の内容を吟味したい。その項立ては以下のようになっている。


(1) 入力値のチェック
(2) 特殊記号のエスケープ
(3) プリペアドステートメントの使用

これらはいずれもSQLインジェクション対策として機能するものだが、この(1)と(2)と(3)の関係は、ANDすなわち「全部やれ」なのだろうか、ORすなわち「いずれかをやれ」なのだろうか。これらは並列に並んでいるだけなので分からない。

一方、安全なウェブサイトの作り方の方は、以下のようになっている。

■ 根本的解決
1)-1 SQL 文の組み立てにバインド機構を使用する
【中略】
1)-2 バインド機構を利用できない場合は、SQL 文を構成する全ての変数に対しエスケープ処理を行う
解説 これは、根本的解決 1) のバインド機構を利用した実装ができない場合に実施すべき実装です。

これなら、バインド機構とエスケープ処理が「どちらか一方」であること、バインド機構を優先して検討すべきことがよく分かる。

実は旧版のセキュア・プログラミング講座の内容を読むと、先の謎が解ける。旧版の方では以下のようになっているのだ

入力値チェックを徹底しよう

任意のSQL文を混入されないためには,入力値チェックを徹底する必要がある。たとえば,

【中略】

しかし人名などの漢字文字コードを扱う場合,本節で紹介した手法では正しい形式かどうかを判断するのは難しい。次節では正しい形式かどうかの判断が困難な入力値を扱う場合について触れる。

入力文字列はエスケープしよう

人名など任意文字を許可する入力文字列を扱う場合,これが任意のSQL文として機能しないようにエスケープする必要がある。

【中略】

以上,SQL文の組み立てにおいて,エスケープ処理の必要性およびその手法について説明した。実はもっと手軽で便利なバインドメカニズムがある。次節ではこれについて説明する。

バインドメカニズムを活用しよう

[SQL組み立て時の引数チェックより引用]

これなら一応分かる。すなわち、第一選択肢としては「入力値チェック」だが「漢字文字コード」の場合はエスケープ、そしてエスケープの簡易便利版として「バインドメカニズム」がある、ということだろう。「安全なウェブサイトの作り方」の方には入力値チェックは対策として示されていないので、両者の推奨内容および優先順位が異なることにはなるが、意図は一応伝わる。

しかるに、新版の方は意図すら伝わらないことから、「駄目な技術文書の見分け方」という意味でよくない。これでは新版は改悪ではないのか。

というわけで、「セキュア・プログラミング講座」に関してIPAにお願いしたいのは以下の3点だ。

  • 技術文書として読んで分かるようにして欲しい
  • 「安全なウェブサイトの作り方」と方法論を統一して欲しい
  • せめて用語だけでも統一して欲しい

続く

_JETエンジンにおいてパイプ記号「|」は今でも「危険」なのか

SQLインジェクション対策の続きで、セキュア・プログラミング講座には、2種類の文字を不受理にせよと書いてある。「;」(セミコロン)と「|」(パイプ記号)だ。

このうち、セミコロンについては不受理にする必要はなく、はっきり間違いといってよいだろう。『3)セミコロン「;」の拒否』の項ではセミコロンを用いた攻撃例も出ているが、脆弱性の原因はセミコロンではなく、シングルクォートをエスケープしていないところにある。バインド機構を用いるか、シングルクォートをエスケープすれば、セミコロンを恐れる必要は全くない。それに、複文を用いた攻撃をもっとも受けやすいMS SQL Serverの場合は、セミコロンなしでも複文を記述できることは前述した。すなわち、原理的にも、現実的にもセミコロンの拒否は意味がない。

一方パイプ記号の方はどうだろうか。少し長いが該当箇所を引用する。

5) その他の特殊記号への対処(Microsoft Jetエンジン)

またMicrosoftのJetエンジンでは、次の文字も機能をもつ特殊記号として扱われる。

  |  VBAステートメント実行文字

Jetエンジンは,与えられたSQL文の文字列の中に「|...|」で囲まれた部分があると、それをVBA(アプリケーション用のVisual Basicサブセット言語)のステートメントとして解釈し実行する。
 これは、SQL 文の中の「'...'」で囲まれた文字列の中に書かれていても起こる。
 これを悪用すると外部からの任意のシステムコマンドの投入が可能となり、最悪の場合、システムが乗っ取られるおそれがある。

このパイプ記号「|」をエスケープする方法は提供されていないため、パイプ記号「|」が含まれている入力パラメータは受理しないようにする必要がある。

  | → 受理しない

Jetエンジンは、Microsoft Accessのデータベースエンジンであるが、直接Access を利用しているつもりがなくても、拡張子「.MDB」をもつデータベースファイルにアクセスする際に使われることになるので注意が必要である。

[SQL組み立て時の引数チェックより引用]

この内容は他の文書類ではあまり見かけない。また、私はACCESSおよびJetエンジンの実務での開発経験がなく、また脆弱性診断などでもお目に掛からないので、今までこの問題を検証しないできたのだが、念のため確認してみた。

この問題に言及している文書としては、2000年2月に塩月誠人氏が公開された「セキュリティ勧告 - NTサーバ上におけるJetセキュリティ問題」がある。

Windows NT上で稼動する、MS Accessデータベース(mdbファイル)にアクセスするようなサーバプログラムは、Jetセキュリティ問題(いわゆる Office ODBCドライバ問題)の影響を受ける可能性がある。このセキュリティ問題の影響を受けるサーバプログラムに対し、悪意を持ったユーザが不正な入力を行なうことにより、サーバマシン上で任意のコマンドが起動する危険性が生じる。

[セキュリティ勧告 - NTサーバ上におけるJetセキュリティ問題より引用]

脆弱なサンプルと検証用文字列は以下のようになっている。

脆弱なサンプル:
  set db=Server.CreateObject("ADODB.Connection")
  db.Open "btcustmr"
  sql="select * from Customers where City='" & word & "'"
  set rs=db.Execute(sql)

検証用文字列:
  |shell("cmd /c echo aaa > c:\test.txt")|   (注: "|" は縦棒文字)

手元のWindows Server 2003を用いて、上記を検証してみたが再現しなかった。パイプ記号は特に不都合なく挿入も検索もできる。塩月氏のレポートから9年も経っているので、Microsoft社が対策したのだろうか。試しにAccess 2003を当該Windows Serverに導入してみたが、現象は変わらなかった(TechNet Plus サブスクリプションを使用)。また、Access 2000 および Access 2002 で、安全でない関数が実行されないように Jet 4.0 を構成する方法などを参考にレジストリをいじったりしたが、やはり現象は再現しない。

このように、現在の環境では「Jetセキュリティ問題」を再現するのは難しいようだが、過去このような問題があったことは確実なので、現在においても「いかなる環境でも絶対に安全」とは断言できない。このような状況下でセキュリティコンサルタントとしてどのようにアドバイスすべきだろうか。

私がJetエンジンを今まで無視してきた理由は、同時に多数が利用するWebアプリケーション構築にJetのようなファイル共有タイプのデータベースエンジンを使用するのは好ましくないと考えるからだ。その方向で技術資料を探してみると、ぴったりそのままの文書が見つかった。

しかし Access ODBC ドライバ、および OLE DB Provider for Jet は、Web システムなどの多くのユーザーからのアクセスによる同時実行や高負荷の対応はされておらず、また、終日稼動で運用されるような高い信頼性を要求されるサーバー アプリケーションで使用されることを考慮して設計されていません。そのため、この様なシステムの場合、弊社では IIS/ASP と共に Microsoft SQL Server、または Microsoft Desktop Engine (以下 MSDE) 等のセキュリティ、常時運用の可用性・信頼性、および拡張性を備えたデータベースの使用を推奨しています。

[IIS 上での Jet データベース エンジンの使用について より引用]

上記のようにMicrosoft自身がWebシステムでのJetの使用を推奨していないのだ。そして、以下の内容が続く。

しかし、Access ODBC ドライバはスレッド セーフではないため、複数のユーザーが同時に MDB ファイル (Access データベース) に要求を行うと、予期せぬ動作が引き起こされる場合があり、システムの安定性に影響を及ぼす可能性があります。そのため、安定性、パフォーマンス、およびスレッド プーリングに対する修正および機能強化を含んでいる OLE DB Provider for Jet の使用を弊社では推奨しています。

注意 : OLE DB Provider for Jet はスレッド セーフですが、Jet がスレッドセーフでないため、完全なマルチ スレッド環境を実装することはできません。そのため、OLE DB Provider for Jet を使用した場合でも、応答がなくなるなど予期せぬ動作が引き起こされる可能性があります。

[IIS 上での Jet データベース エンジンの使用について より引用]

色々書いてあるが、「Jet がスレッドセーフでない」という箇所が重要だ。このことから、Jetを使用したWebアプリケーションでは「別人問題」のように、他ユーザの情報が漏洩する可能性などが考えられる。

まとめよう。「Jetセキュリティ問題」は過去の問題とは言い切れないかもしれないが、少なくとも現在では非常にトリビア的な問題だ。しかも、JetをWebシステムでは使うべきでないとMicrosoft自身が明記しているのだ。であれば、セキュア・プログラミング講座で説明すべきことは、「パイプ記号を受理しない」ではなく、こうだろう。

  例えば、Jetエンジンを避ける

参考:WASForum Conference 2008講演資料「SQLインジェクション対策再考」

2009年6月16日追記

本エントリを書いた後、セキュアプログラミング講座の内容は大きく改訂されたようで、ここで指摘した問題はすべて解消されている。関係者の皆様、ありがとうございました。


2009年03月11日 文字コードのセキュリティ問題はどう対策すべきか

_U+00A5を用いたXSSの可能性

前回の日記では、昨年のBlack Hat Japanにおける長谷川陽介氏の講演に「趣味と実益の文字コード攻撃(講演資料)」に刺激される形で、Unicodeの円記号U+00A5によるSQLインジェクションの可能性について指摘した。

はせがわ氏の元資料ではパストラバーサルの可能性を指摘しておられるので、残る脆弱性パターンとしてクロスサイト・スクリプティング(XSS)の可能性があるかどうかがずっと気になっていた。独自の調査により、XSS攻撃の起点となる「<」や「"」、「'」などについて「多対一の変換」がされる文字を探してきたが、現実的なWebアプリケーションで出現しそうな組み合わせは見つけられていない。

一方、U+00A5が処理系によっては0x5C「\」に変換されることに起因してXSSが発生する可能性はある。JavaScriptがからむ場合がそれだ。しかし、実際にXSS脆弱性が発生するには、次のような状況を想定する必要がある。

  • 入力(HTTPリクエスト)はUnicodeで受け取る
  • 内部の処理もUnicodeで行われる
  • 出力(HTTPレスポンス)のエンコーディングはShift_JISあるいはEUC-JP

すなわち、入力(リクエスト)と出力(レスポンス)で異なる文字エンコーディングを想定しなければならない。これ自体は現実性が薄い。

この問題については、既に佐名木氏らの研究がある。佐名木氏は、Apache Tomcatに着目して以下のように記述している。

実験のポイントは、Tomcat の仕様変更である。Tomcat4 系からTomcat5 系へのバージョンアップによって、クエリー文字列は常にUTF-8 として受け取るように仕様が変更された。

この仕様変更に着目することで、ANSI の世界でデータ処理、そしてデータ出力を行っているJavaServlet に対してUTF-8 の世界の文字を与えることができる。

[Unicodeとサニタイジング回避テクニック ver1.6より引用]

Tomcatのクエリ文字列の文字化け問題はFAQであって、現実には「常にUTF-8として」受け取られるわけではなく、server.xmlの設定により、useBodyEncodingForURI="true" (クエリ文字列の文字エンコーディングをPOSTのエンコーディングと一致させる)を指定することができる。従って、佐名木氏の指摘しておられる状況もなくはないだろうが、もう少し現実的に *ありそうな* 可能性を検討したい。

そこで私は、HTTPリクエストの文字エンコーディングを「自動認識」させている場合に注目して調査を行った。各処理系に対する考察を以下に述べる。

Javaの場合

Javaは前述のように、U+00A5が\x5Cに変換されるので有力な候補だが、J2SEの文字エンコーディング自動判定機能は、JIS系の文字エンコーディングの範囲で行われる(JISAutoDetect)ため、上記の条件を満たすことができない。文字エンコーディングの自動判定を自作することは可能だが、検討の対象からは外すことにした。

PHPの場合

PHPは文字エンコーディングの自動判定が柔軟だが、一方、U+00A5が全角の円記号「¥」に変換されるため、XSSには利用できない。

Perlの場合

PerlにはJcode.pmやEncode.pmに文字エンコーディングの自動判定機能がある。しかし、UnicodeからShift_JISなどへの変換に際してU+00A5が「?」に変換されるため、やはりXSSには利用できない…と思っていた。最近までは。

しかし、私がITproに連載している連載中の記事「第6回■異なる文字集合への変換がぜい弱性につながる 」に対して、id:nihenさんからブックマークコメントを頂戴した。

【Perl(Encode.pm).(略).では発生しない】cp932では発生するです。http://cpansearch.perl.org/src/DANKOGAI/Encode-2.31/ucm/cp932.ucm

すなわち、UTF-8からShift_JISへの変換だとU+00A5は「?」に変換されるが、cp932(Windowsの機種依存文字を考慮したShift_JIS)への変換の場合は「\」(\x5C)に変換されるというのだ。確認したところ、たしかにそうなる。これで、U+00A5によるXSSの可能性が出てきた。さっそく試してみよう。

以下にサンプルコードを示す。

#!/usr/bin/perl
use CGI;
use utf8;
use Encode;
use Encode::Guess qw/utf8 shiftjis euc-jp/;

my $query = CGI->new;
my $p = decode 'Guess', $query->param('p');
# 制御文字のチェック…省略
# 次の行はJavaScript文字列リテラルのエスケープ
$p =~ s/(?=[\\\'\"])/\\/g;     # \ → \\  ' → \' " → \"
$p = $query->escapeHTML($p);   # HTMLエスケープ

print encode 'cp932', <<EOT;
Content-Type: text/html; charset=Shift_JIS

<html>
<body onload="alert('$p');">
テスト
</body></html>
EOT

この簡単なスクリプトは、クエリストリングpの値をalertダイアログに表示するだけの簡単なものだ。処理の流れは以下のようになる。

  • 文字エンコーディングの自動判定候補として、UTF-8、Shift_JIS、EUC-JPを指定
  • クエリストリングpを読み込み、文字エンコーディング自動判定でUTF-8に変換
  • JavaScript文字列リテラルとしてのエスケープ
  • HTMLエスケープ
  • 文字エンコーディングcp932を指定してHTML生成
  • body要素のonloadイベントハンドラにalert関数を生成

JavaScriptの動的生成に対して必要なエスケープ処理については、過去にXSS対策:JavaScriptのエスケープ(その2)などで説明した通りである。

私はそもそもJavaScriptの動的生成を推奨していないが、イベントハンドラにJavaScriptを置く場合は比較的シンプルに考えられる。上記のように、JavaScriptとしてのエスケープとHTMLのエスケープを2段階で行えばよい。面倒ではあるが、SCRIPT要素に置く場合のようにデータの途中に</SCRIPT>が出てくるような特殊ケースは考えなくて良い。

このスクリプトに対して、以下のような入力を与える(U+00A5は赤色全角の円記号で記述)。

');alert(document.cookie);//

この文字列は以下のように処理される。まずJavaScriptのエスケープ処理(' → \')。

\');alert(document.cookie);//

次にHTMLエスケープ(' → &#39;)

\&#39;);alert(document.cookie);//

そしてcp932に変換( → \)

a\\&#39;);alert(document.cookie);//
この文字列はJavaScriptの実行に際して、HTMLデコードされ以下のようになる。
alert('a\'');alert(document.cookie);//');"

すなわち、JavaScriptの文字列リテラルが突破され、第二のalertが追加された。XSSの成功である。

対策

この問題の根本原因は、UTF-8→cp932(Shift_JIS)の変換に伴う多対一変換にある。従って、文字エンコーディングを全てUTF-8に統一することで根本対策となる。しかし、一般的には、携帯電話や電子メールなど、JIS系文字集合を使わざるを得ない場合もあり、ブラウザとのやりとりはShift_JIS(あるいはEUC-JP)、プログラム内部ではUTF-16やUTF-8というケースは多いだろう。

このような場合は、プログラムの実行環境はUnicodeだが、処理対象となる文字集合をJIS系文字集合(マイクロソフト標準キャラクタセットなど)に限定することで対策が可能だ。具体的には、入力時の文字エンコーディング自動判定をやめ、エンコーディングを明示することだ。だが、もっとよい方法があるかもしれない。

一つの可能性として、長谷川氏の講演資料に指摘されているような「検査後には変換しない」すなわち、変換してから検査(エスケープ)する方法もある。しかし、PerlはShift_JISの文字列処理には対応していないので、cp932(Shift_JIS)に変換してからのエスケープも容易ではない。

また、「もっとよい方法」とは、従来言われてきた「過剰エスケープ」を指すわけではない。上記の例で言えば、スラッシュ「/」を「\/」にエスケープすれば、JavaScriptのコメント「//」が有効でなくなり、JavaScriptの実行エラーになるので攻撃は成立しない。しかし、そのような対策はアドホックで、理論的な裏付けに乏しいものだ。

また、今回は取り扱わないが、文字エンコーディングを利用した攻撃についても同様のことが言える。PerlのEncode::decodeは文字エンコーディングのチェックを行うので、そもそも不正な文字エンコーディングについては除去してくれる。エラーにしたければ(そうするべきだが)、オプションの第3パラメータCHECKにEncode::FB_CROAKを指定すればよい。それでも残る問題は処理系のバグ(脆弱性)なのだ。処理系のバグに対してアプリケーション側で対策しなければならない場合もあるだろうが、それは原因の所在を明確にした上での話だ。

文字コードのセキュリティ問題に関しては、公開されている情報が非常に少ない。とくに文字集合をアプリケーション開発の際にどのように取り組むべきかという議論はほとんどなされていないように思う。私は、ITproの連載の中で私見を述べているが、先行する研究がほとんどないので原理から手探りで検討している状態だ。この問題に対する活発な議論が行われることを期待する。


2008年12月22日

_JavaとMySQLの組み合わせでUnicodeのU+00A5を用いたSQLインジェクションの可能性

今年のBlack Hat Japanには、はせがわようすけ氏が「趣味と実益の文字コード攻撃」と題して講演され話題となった。その講演資料が公開されているので、私は講演は聞き逃したが、資料は興味深く拝見した。その講演資料のP20以降には、「多対一の変換」と題して、UnicodeのU+00A5(通貨記号としての¥)が、他の文字コードに変換される際にバックスラッシュ「\」(日本語環境では通貨記号)の0x5Cに変換されることから、パストラバーサルが発生する例が紹介されている。

しかし、バックスラッシュと言えばSQLインジェクションの可能性も見逃すことができない。そこで、本資料をきっかけとして、U+00A5を使ったSQLインジェクションの可能性について調査し、Java(JDBC)とMySQLの組み合わせにおいて、発生する場合があることを確認したので報告する。

U+00A5を用いたSQLインジェクションとは

ここで、U+00A5を用いたSQLインジェクションとはどのようなものかを説明しよう。UnicodeのU+00A5はバックスラッシュとは独立に扱える日本円の通貨記号として割り当てられている。この文字をShift_JISやEUC-JPなどに変換する際に、ASCIIの0x5Cに変換される(場合がある)。すると、バックスラッシュをSQLのエスケープに使用するデータベース、具体的にはMySQLとPostgreSQLにおいて、SQLインジェクションが発生する場合がある

具体例を用いて説明しよう。検査パターンとして以下の文字列を使用する。以下、U+00A5を表記する場合には赤色全角の通貨記号「」を用いる

'OR 1=1#

先頭の文字がU+00A5である。これをMySQLのルールでエスケープすると、シングルクォートが「\'」と変換され、以下のようになる。

\'OR 1=1#

ややこしいが、最初の通貨記号がU+00A5、二番目の通貨記号が0x5Cである。これをShift_JISあるいはEUC-JPに変換すると以下のように、二文字とも0x5Cになる。

\\'OR 1=1#

これをSQLとして解釈すると、最初の「\\」が「\」をエスケープしたものと見なされ、「'」はエスケープされない状態となる。すなわち、SQLインジェクションされたことになる。

どのような場合に問題になるか

このタイプのSQLインジェクションが発生するのは、以下のようなケースが典型的な場合であろう。

  • 外部とのインターフェースにUnicode(典型的にはUTF-8)を用いていて、U+00A5を入力することができる
  • アプリケーションの内部でもUnicode(UCS-2、UTF-16、UTF-8など)を用いている
  • SQLのエスケープはUnicodeの状態で実行している
  • アプリケーションからデータベースのクエリ実行までのどこかで、Unicode以外の文字コード(典型的には、Shift_JISかEUC-JP)に変換されている

内部コードとしてUnicodeを用いる言語は現在では数多いが、筆者はJavaとPerl(use utf8;)を用いて検証した。その結果、JavaとMySQLの組み合わせの場合にSQLインジェクションが発生する場合があることを確認した

検証コードの説明

以下のような検証コードを用いてテストした。

import java.sql.*;
public class MyA5Injection {
  public static void main(String[] args) {
    try {
      String charEncoding = "sjis";    // or "utf8"
      Class.forName("com.mysql.jdbc.Driver");
      Connection con = DriverManager.getConnection(
        "jdbc:mysql://localhost/tokumaru?user=xxx&password=xxxx&useUnicode=true&characterEncoding=" + charEncoding);
      Statement stmt = con.createStatement();

      String param = "\u00a5'or 1=1#";

      // MySQL用のエスケープ
      String e_param = param.replaceAll("\\\\", "\\\\\\\\");    // \ → \\
      e_param = e_param.replaceAll("'", "\\\\'");               // ' → \'

      String sql = "SELECT * FROM test WHERE name='" + e_param + "'";
      System.out.println("sql = " + sql);
      ResultSet rs = stmt.executeQuery(sql);
      while(rs.next()){
        int id = rs.getInt("id");
        String name = rs.getString("name");
        System.err.println(id + " " + name);
      }
      stmt.close();
      con.close();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

実行結果は以下の通り

C:\HOME\Java>java MyA5Injection
sql = SELECT * FROM test WHERE name='\\'or 1=1#'
~ 検索結果の表示 ~

テスト結果

U+00A5を用いたSQLインジェクションは、JDBCのgetConnectionメソッドに指定するオプションパラメータcharacterEncodingに依存するようだ。このパラメタがUTF-8の場合はSQLインジェクションは発生しない。一方、Shift_JISやEUC-JPの場合はSQLインジェクションが発生する。create tableのdefault charset設定には依存しないようだ。これらを下表にまとめた。

UTF-8のテーブル Shift_JISのテーブル
characterEncoding=utf8 正常処理 エラー(*1)
characterEncoding=sjis SQLインジェクション SQLインジェクション
検証に用いた環境
MySQL 5.0 および 5.1
MySQL Connector/J 5.1.7
JDK6 Update11
Windows XP Professional 

(*1) java.sql.SQLException: Illegal mix of collations (sjis_japanese_ci,IMPLICIT) and (utf8_general_ci,COERCIBLE) for operation '='

現実的に脆弱となる組み合わせはどの程度使用されているか

現実にSQLインジェクションが発生するは、JavaとMySQLの組み合わせすべではなく、characterEncodingの指定が明示的あるいは暗黙にutf8以外の値になっている場合と考えられる。筆者が試した範囲では、MySQLのコンフィグレーション・ウィザードで「Best Support for Multilingualism MySQL」を指定した場合にはUTF-8が利用されるが、それ以外の場合はlatin1、あるいはユーザが指定した文字エンコーディング(Shift_JISなど)が設定される。また、GoogleでgetConnectionを検索すると、characterEncoding=sjisと記述した例が多数ヒットしている。そのような状況では、characterEncodingとしてUTF-8以外が指定されている比率は割合に多いのではないかと予想する。

その他の言語とDBの組み合わせの場合はどうか

筆者が他の組み合わせで試した範囲では、Java+PostgreSQLやPerl+MySQLではSQLインジェクションにはならなかった。Java+PostgreSQLの場合はエラーになり、Perlの場合はU+00A5が「?」に変換されるようで、やはりSQLインジェクションにはならなかった。しかし、筆者が試したものと別の条件ではSQLインジェクションが発生する可能性はゼロではない。

対策

はせがわようすけ氏の講演資料には以下のような対策が推奨されている

  • Unicodeのまま文字列を扱い、変換しない
  • (変換するとしても)検査後には変換しない

SQLインジェクション対策としても「変換しない」というガイドラインは有効である。すなわち、以下を推奨する。

  • characterEncoding=utf8を明示する(必須)
  • create tableの際のdefault charsetにもutf8を設定する(推奨)

追記(2008/12/22 14:00)

金床氏から「例のコードがPreparedStatementじゃないのは何故だろう」という指摘を受けた。原理を示すためにはエスケープの方が分かりやすいと思ったからだが、PreparedStatementでも試してみた。主なコードの変更点は以下の通り(エスケープ処理は必要なくなる)。

      String sql = "SELECT * FROM test where name=?";
      PreparedStatement stmt = con.prepareStatement(sql);
      stmt.setString(1, param);
      ResultSet rs = stmt.executeQuery();

結果は、エスケープの時とまったく同じであった。MySQL 5.1でも直っていない…というか、これは仕様かもしれない。やはり、文字エンコーディングはアプリからDBまでそろえよう。

追記(2008/12/24 00:00)

へぼへぼCTO日記さんからトラックバックを頂戴した。Connector/JでサーバーサイドのpreparedStatementを使用するには、オプションuseServerPrepStmts=trueを指定しなければならないとのこと。手元の環境でテストしたところ、同オプションを指定したところU+00A5によるSQLインジェクションは再現しなくなった。ご指摘ありがとうございます。

追記(2009/07/17 14:00)

SH2さんのブログによると、MySQL Connector/J 5.1.8にてこの問題は修正されたようです。ありがとうございました。



[PR]小規模ECサイトに最適なWAF、SiteGuard Lite

ockeghem(徳丸浩)の日記はこちら
HASHコンサルティング株式会社

最近の記事

最近のツッコミ

  1. 海老原昂輔 (10-10)
  2. 通りすがり (09-19)
  3. otsune (09-19)
Google