エスケープしてもSQLインジェクション対策にならない
.NET Frameworksを利用するWebアプリケーションでの、文字コードの扱いが謎です。Webページの表示とFormへの入力だけならともかく、DBを使ったりするとよく解らんのです。
基本的に.NET Frameworksの内部*1ではUnicode*2が使われていて、生成されるWebページはUTF-8になっているようです。この場合にユーザからの入力を受け付けるページでは
ユーザの入力 ↓UTF-8 ASP.NET ↓Unicode Webアプリケーション(.NET Frameworks)
という感じで文字コードの変換が行われているようです。
ここにEUC-JPが使われているMySQLへのODBC接続が加わると
ユーザの入力 ↓UTF-8 ASP.NET ↓Unicode Webアプリケーション(.NET Frameworks) ↓Shift_JIS MyODBC日本語変換機能版(EUC変換をする)*3 ↓EUC-JP MySQL
という動作をします。
よく分からないのはMyODBCに渡される前に行われるShift_JISへの変換部分で、アプリケーション(というか.NET Frameworks?)が自動的に変換しているみたいなのです。
が、Unicodeでは日本語以外の各国語を扱えるのに対してShift_JISでは日本語(と英語)しか扱えません。例えばロシア語圏とか韓国語圏とかで動いているアプリケーションがUnicodeからShift_JISに変換する事は無いはずです。
そうなると何らかの情報を元に、変換する文字コードとしてShift_JISを選択しているはずですが、その情報というのは一体何なのでしょうか。OSの言語(日本語版とか)?.NET Frameworksの言語(同じく)?OSのローケル設定?文字自体が日本語の場所にあるから?
さて、猛烈に長い前置きがありましたがここからが本題です。
2005年は上半期に色々あったおかげで、SQLインジェクションが注目される事となりました。対策方法について書かれた記事は多くありますが、簡単にまとめると適切にエスケープするかバインドメカニズムを使用せよという内容です。
以前.NET C#で、EUC-JPで構築されたMySQLにMyODBCで接続するアプリケーションを作ったことがあります。その時はエスケープする方法を採りました。そのアプリケーションではユーザが入力したIDとパスワードを含むSQLを生成して実行するのですが、IDとパスワードにはASCII範囲外の文字が許可されていないことから、MyODBCは日本語変換機能版ではなく生のMyODBCを使用していました。
エスケープは
\ → \\ ' → \' 0x00 → 削除
という標準的(?)な方法です。ここまで書くと勘の良い方は気が付きそうですが、この場合エスケープしてもSQLインジェクションが成立します。
説明するための例としてユーザのパスワードを変更するアプリケーションを考えてみます。
USER_ACLテーブル
USER_NO | USER_ID | PASSWORD |
---|---|---|
1 | admin | password |
2 | foo | bar |
3 | example | example |
ユーザが入力する画面の項目をユーザIDと新旧パスワードだけにし、実行するSQLを
UPDATE USER_ACL SET PASSWORD = 'new_password' WHERE USER_ID = 'userid' AND PASSWORD = 'old_password';
とします。
ユーザがIDに「ミソ」、旧パスワードに「 OR USER_NO = 1 # 」*4、新パスワードに「a's」と入力します。
ユーザの入力 ↓UTF-8 ↓ ID: ミソ(0xE3839F 0xE382BD) ↓ 旧PW: OR USER_NO = 1 # ↓ 新PW: a's ASP.NET ↓Unicode ↓ ID: ミソ(0x30DF 0x30BD) ↓ 旧PW: OR USER_NO = 1 # ↓ 新PW: a's Webアプリケーション(.NET Frameworks) ↓Shift_JIS ↓ UPDATE USER_ACL SET PASSWORD = 'a\'s' ↓ WHERE USER_ID = 'ミソ(0x83 0x7E 0x83 0x5C)' ↓ AND PASSWORD = ' OR USER_NO = 1 # '; MyODBC ↓Shift_JIS ↓ UPDATE USER_ACL SET PASSWORD = 'a\'s' ↓ WHERE USER_ID = '???\' ↓ AND PASSWORD = ' OR USER_NO = 1 # '; MySQL(EUC-JP)
結果として
USER_NO | USER_ID | PASSWORD |
---|---|---|
1 | admin | a's |
2 | foo | bar |
3 | example | example |
adminのパスワード知らなくてもa'sに変更できてしまいました。
いわゆるShift_JISの5C文字問題がこんなところで顔を出したのです。Unicodeの文字列に対していくらエスケープをしても、Shift_JISに変換されると「\(0x5C)」がエスケープされない形で現れてしまいます。Shift_JISに変換される認識が薄いために起きた出来事と言えます。
他のDBやODBCドライバを使った場合にどうなるかは調べていませんが、今回のようなマニアックな組み合わせではエスケープしてもSQLインジェクション対策になりませんでした。幸いバインドメカニズムも使用可能でしたので、おとなしくそちらを使うことにしたのでした。
というわけで、使えるならバインドメカニズムを使いましょう。あと、誰か.NETの挙動について教えてください。
追記
ちなみにここでのMySQLはバージョン3.xです。4.1.x以降ではサーバ側でキャラクタセット文字エンコーディングの変換を行うため事情が変わってきます。
もういっちょ追記
高木さんの日記を見て書こうかどうか悩んで結局書かないでいた事を思い出したんですが、普通は例のように平文でパスワードを保存しちゃだめです。
さらに追記
Visual Studio 2003の初期状態ではaspxファイルに記述された日本語はShift_JISになっています。で、初回実行時などのタイミングで.NETの内部形式に変換され、出力はUTF-8で行われています。この時、
コントロール パネル→地域と言語のオプション→詳細設定→Unicode 対応でないプログラムの言語
で設定された値を参照して変換しているみたいですので、ODBCに文字を渡す際の変換にもこの設定が関係ありそうです。