2008年12月22日月曜日

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

補足

この記事は旧徳丸浩の日記からの転載です(元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になる。
\\'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にてこの問題は修正されたようです。ありがとうございました。

フォロワー

ブログ アーカイブ