文字集合自体は抽象的な「文字の集まり」に過ぎないので単独で問題になることはないが,異なる文字集合に変換する際には問題が発生する場合がある。文字集合が異なるということは,対応する文字が1対1対応していないので,変換先の文字集合で対応する文字がないケースや,多対1の対応が発生する可能性がある。

 図1に,Unicodeからマイクロソフト標準キャラクタセットに変換する場合を例示した。マイクロソフト標準キャラクタセットには「骶」(尾てい骨の“てい”)や,ハングルなどはない。また,バックスラッシュ「\」(U+005C)と円記号「\」(U+00A5)がともにJIS X 0201の「\」(0x5C)に変換される場合について示している。

図1●異なる文字集合への変換

 「漢」のように1対1対応している文字は問題ない。ハングルや「骶」のように対応するコードポイントがない場合はエラーになるか文字化けする。インターネットで「尾 骨 びていこつ」というキーワードで検索すると「尾?骨」などと文字化けして表示されているコンテンツがヒットする。おそらく,Unicode文字集合からJIS系の文字集合への変換が行われた結果,「骶」が文字化けしたものと予想される。このような文字化けはソフトウエアの動作としては問題である。

 セキュリティ上の問題が発生しやすいのは,「\」と「\」のように,複数の文字が一つの文字に変換される「多対一の変換」の場合である。多対一の変換がなぜセキュリティ上の問題になるのか。以下,具体例によって説明しよう。

■ ユニコードのU+00A5によるSQLインジェクション
 多対一の変換が現実に発生するか否かは処理系依存であって,個別に検証する必要がある。先に示した「\」U+005Cと「\」U+00A5がJIS X 0201への変換に際して共に「\」0x5Cに変換される例は,Java(Java SE 6にて確認)およびPerl(Encode.pmにて確認)では発生するが,PHP(PHP 5.2.8にて確認)では発生しない。

修正履歴(2009.2.25)
元々は「Perlでは発生しない」としていましたが,誤りがありましたので訂正しました。Perlで「\」U+005Cと「\」U+00A5がJIS X 0201への変換に際して共に「\」0x5Cに変換されないのは,変換先に'shiftjis'を指定した場合です。'cp932'(Windows上の機種依存文字を考慮したShifjt_JIS)や'macJapanese'(Macの機種依存文字を考慮したShift_JIS)では発生します。

 Javaでの「多対一の変換」の影響として,SQLインジェクションぜい弱性が発生する例を示そう。以下は,クエリストリングの値をキーとしてSQLにより検索するServletの断片である。


import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.sql.*;

public class TestSql extends HttpServlet{
   public void service(HttpServletRequest request, 
                                HttpServletResponse response)
                throws ServletException, IOException {
response.setContentType("text/html; charset=UTF-8"); // レスポンスをUTF-8に
    request.setCharacterEncoding("UTF-8");  
                                     // リクエストのエンコーディングをUTF-8に
    String param = request.getParameter("p");  // パラメータpを取得

    try {
      Class.forName("com.mysql.jdbc.Driver");      // MySQL用JDBCドライバ
      Connection con = DriverManager.getConnection(
"jdbc:mysql://hostname/dbname", db_user, password);
Statement stmt = con.createStatement();

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

      String sql = "SELECT * FROM mytable WHERE param='" + e_param + "'";
      ResultSet rs = stmt.executeQuery(sql);
      // 以下,結果の表示など

「MySQL用のエスケープ」とコメントしているところで,SQLインジェクション対策として必要なシングルクオート「’」とバックスラッシュ「\」のエスケープ処理を実行しているが,MySQLの設定によってはSQLインジェクションの危険性があるのだ。

 具体的には,MySQLとの接続に用いる文字エンコーディングがUnicode系でない場合に,以下のURLによってSQLインジェクションが発生する。

http://host/TestSql?p=%C2%A5’or+1%3d1%23

 パーセントエンコードされていて分かりにくいので下図に文字コードと文字を図示する。Javaに処理が移った時点でUTF-16にエンコーディングされているが、下図では上位のU+00を省略している。

エスケープはシングルクオートを重ねる処理で結果は以下のようになる。

ここまでは問題ないのだが,MySQLの設定によっては,UTF-16からEUC-JP(あるいはShift_JIS)に自動変換され,先頭のU+00A5が0x5Cに変換される。

この時点のSQL文を図示するとこうだ。

 U+00A5「\」が,異なる文字集合への変換の結果,0x5C「\」に変換されたために,後続のシングルクオートのエスケープのために使われ,その後ろのシングルクオートが文字列リテラルを閉じるために使われる結果となった。そのため,それ以降の入力は,あらたなSQLを注入(インジェクション)するために使われたのである。

■ 暗黙の文字コード変換の危険性
 このような危険性が発生するのは,SQL問い合わせだけではない。表示(HTTPレスポンス),電子メール(SMTP),ファイル入出力,コマンド呼び出しなど,様々なインタフェースを呼び出す際に「自動文字コード変換」が行われる。都合の悪いことに,この自動変換は,アプリケーションの制御の及ばないところで行われるため,対策が難しい。

■ 文字集合は変換しないことが原則
 異なる文字集合への変換は文字化けなどを引き起こし,ぜい弱性の原因となる場合がある。したがって,やむを得ない場合を除いて文字集合を変換しないという原則を貫くことが重要だ。

 先のMySQLの例では,接続文字列に文字エンコーディングを明示することで対策できる。


Connection con = DriverManager.getConnection(
"jdbc:mysql://hostname/dbname?characterEncoding=utf8", …

 さらに,以下のようにMySQLの設定ファイル(my.cnfあるいはmy.ini)に以下を設定する。


[mysqld]
default-character-set=utf8

 詳しくは「SQLアクセス」の項で詳しく説明する。

●まとめ
 アプリケーションで扱う文字集合の問題として、異なる文字集合への変換において,「多対一の変換」が発生するとセキュリティ上の問題となる場合があることを説明した。次回は、文字エンコーディングにまつわる問題点について説明する。

Webアプリケーションのセキュリティに関する研究,コンサルティングを手掛ける。京セラ コミュニケーションシステムで文書管理を中心とするパッケージ・ソフトの企画・開発ののち,Webセキュリティ関連の業務に従事。2008年4月に独立。