補足
この記事は旧徳丸浩の日記からの転載です(元URL、アーカイブ、はてなブックマーク1、はてなブックマーク2)。備忘のため転載いたしますが、この記事は2008年12月22日に公開されたもので、当時の徳丸の考えを示すものを、基本的に内容を変更せずにそのまま転載するものです。
補足終わり
今年の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になる。これをSQLとして解釈すると、最初の「\\」が「\」をエスケープしたものと見なされ、「'」はエスケープされない状態となる。すなわち、SQLインジェクションされたことになる。\\'OR 1=1#
どのような場合に問題になるか
このタイプの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のまま文字列を扱い、変換しない
- (変換するとしても)検査後には変換しない
- characterEncoding=utf8を明示する(必須)
- create tableの際のdefault charsetにもutf8を設定する(推奨)
追記(2008/12/22 14:00)
金床氏から「例のコードがPreparedStatementじゃないのは何故だろう」という指摘を受けた。原理を示すためにはエスケープの方が分かりやすいと思ったからだが、PreparedStatementでも試してみた。主なコードの変更点は以下の通り(エスケープ処理は必要なくなる)。結果は、エスケープの時とまったく同じであった。MySQL 5.1でも直っていない…というか、これは仕様かもしれない。やはり、文字エンコーディングはアプリからDBまでそろえよう。String sql = "SELECT * FROM test where name=?"; PreparedStatement stmt = con.prepareStatement(sql); stmt.setString(1, param); ResultSet rs = stmt.executeQuery();