2008年05月04日 16時58分52秒
Webアプリが外部に公開されているから事例が多く発生して見えるだけです。
人知れず自分の担当する「Webアプリではないシステム」でも起きている可能性が大きいのです。
だらだら書いてます。文章構成とか気にしてないので読みにくいかも。
あるBIプロダクト。
SQL中に外部からの入力を埋め込むマクロが備わっています。
使い方は
SELECT *
FROM PROD_MASTER
WHERE PROD_CATEGORY = '1'
#PromptMacro(value1,"AND PROD_CD=")#
→ value1 に xxx を設定する
SELECT *
FROM PROD_MASTER
WHERE PROD_CATEGORY = '1'
AND PROD_CD=xxx
のような感じ。
value1に値がまだ設定されていない場合は、プロンプトが表示され値の入力を促します。設定されると、以降他のvalue1を使うマクロは入力された値を適用してSQLを生成します。
よくある機能ですね。
ところでこれ、value1の入力値が検証できないと非常に危険です。マクロでは入力値の妥当性チェックが出来ないから、xxxが
"1 OR PROD_CD=2"
だったりすると…
SELECT *
FROM PROD_MASTER
WHERE PROD_CATEGORY = '1'
AND PROD_CD=1 OR PROD_CD=2
PROD_CD で指定する値が二つ以上だとこのクエリの結果はどうなるかわかりません。もし項目名が推測できちゃったりして本来見せちゃいけない条件の指定が出来たりしたら….
これはいわゆるSQLインジェクションというやつです。SQLもしくはSQLの一部を注入(インジェクション)するわけですね。
この例では、マクロの展開位置によっては全件抽出も出来るかもしれませんし、DELETE文を紛れ込ませてデータ消去もできるかもしれません。
→ value1 に "1 ; DELETE FROM PROD_MASTER" を設定する
SELECT *
FROM PROD_MASTER
WHERE PROD_CATEGORY = '1'
AND PROD_CD=1 ; DELETE FROM PROD_MASTER
これも安直なWeb上のサンプルでコードを書くと発生するSQLインジェクションの例。
以下に仮想的なコードを示します。
//// import data
var rdbObj = new rdbObject("id=xxx;pass=yyy;db=zzzz");
var fileObj = new fileObject("D:\data.csv","r");
var sql = "";
var data1,data2;
while(fileObj.endOfFile != true) {
fileObj.read(data1,data2);
sql = "INSERT INTO PROD_DATA(ITEM_CD,QTY) VALUES('" + data1 + "'," + data2 + ");";
rdbObj.execute( sql );
}
rdbObj.close();
fileObj.close();
data.csv から読み込んだ値を、 データベースの PROD_DATA テーブルへ INSERT しています。
もし、data.csv ファイルの1行目が
"ABC","100"
であれば、
INSERT INTO PROD_DATA(ITEM_CD,QTY) VALUES('ABC',100);
のSQLが作られ、実行されます。
data.csv ファイルの各行毎に上記のようなSQLが作られ、実行されていきます。
よくありがちな例です。
さて。
もし data.csv ファイルの1行目が
"',100); DELETE FROM PROD_DATA; --","200"
だとどうなるでしょう?
INSERT INTO PROD_DATA(ITEM_CD,QTY) VALUES('',100); DELETE FROM PROD_DATA; --',200);
↓ SQLが二つ組み込まれている
INSERT INTO PROD_DATA(ITEM_CD,QTY) VALUES('',100);
DELETE FROM PROD_DATA; --',200);
これもSQLインジェクションです。
入力値が数値であれば変数 data1 に入る値を簡単に検証できるでしょう。しかし、文字列だとすると検証はかなり難しくなります。
プログラム仕様書で「入力したら保障できないデータ」をあげるのも結構ですが全組み合わせを書き出すことなんてまず出来ません。
それに、普通は保障できないデータは弾くとか検出機構を作りこむ必要があるでしょう。
SQLを文字列の形で動的編集できる機能は危険を抱えてます。
外部の信用ならないデータを取り込む
編集したSQL自体の信用検証
これをパスして初めて実行可能となります。
プログラムの内部で、プログラム作成者の意図に従って生成したデータ(文字列)で構成するSQLはある程度信用できるSQLです。
少なくとも意図的に妨害や損傷を与えるSQLを作る事はしない筈です。
ですが、ファイルやデータベーステーブルやユーザの入力等、プログラムの外部から取り込む、予期しない内容を入力してくる可能性がある場合、これを信用できないデータ、もしくは汚染されたデータと(勝手に)呼びます。
みんな良い人ならいいのですが、そうでない人もいるので、意図的に誤動作を誘発するようなデータ内容を送ってくる可能性があります。
外部プログラムも、もしかしたら悪意を持った内容を送り込んでくるかもしれません。
当然、入力値はきちんと検証を行い、プログラムの仕様に合ったものなのかを確認します。もちろん仕様を外れていればエラーにしなくてはいけません。
でも、仕様に合うのに問題がおきる場合もあります。
特にSQLを文字列の連結で作るような場合、入力値内にシングルコーテーション(')やダブルコーテーション(”)があるような時はエスケープ処理を行い文字列中に含ませても問題がないよう対処しなければいけません。コメント開始の文字(–)やマルチステートメント指示(;)も出現場所によっては厄介ですね。
入力値のせいでSQLが成り立たなくなるのはまずいですから、単にエスケープ処理で終わる訳ではなく、全部連結した結果SQLとして正しい体裁を為すように補正する場合もあります。
…おかしいですよね。入力値そのものの検証ではなく、それを組み入れたSQLも検証しなくてはいけないなんて。
入力値の検証にコストをかけて、かつSQLの構成確認にコストをかけて…
世の中には「サニタイズ」なる用語があるようです。総じてこの用語を使っている書籍やページが言うには「特殊文字はちゃんとサニタイズ(?)して無力化しておけば問題ないぜ」と。
しかしほとんどの場合、
な環境しか考慮に入っていません。
※SQLの問題と、ブラウザ表示時の問題がごちゃごちゃに語られている気がする
別にやってもいいんですけどね。苦労の割には本来の処理に全くといってよいほど貢献できません。
これは、彼らの言うところの「サニタイズ」が小細工の部類で、元々潜んでいる問題を全く解決していないからです。
そもそも、入力値を含めてSQLとして解析しようとするから問題が起きます。だったら、SQLの内容が入力値に左右されない様にしちゃえば良いわけです。
そのやり方の一つにプレースホルダという仕掛けがあります。RDBに限らず、こういうやり方はいろいろなところに使われています。
昨今のWebアプリのSQLインジェクションは、そういう機能を知らずに(教わらずに)きた技術者によって引き起こされていると思ってもいいのではないかと思います。
まぁ、教えられる技術者も居なくなってきているという話もあるのですが。
以下に仮想的なコードを示します。
//// import data
var rdbObj = new rdbObject("id=xxx;pass=yyy;db=zzzz");
var fileObj = new fileObject("D:\data.csv","r");
var sql = "";
var data1,data2;
rdbObj.prepare("INSERT INTO PROD_DATA(ITEM_CD,QTY) VALUES(@itemcd, @qty);");
while(fileObj.endOfFile != true) {
fileObj.read(data1,data2);
rdbObj.parameter("@itemcd", data1);
rdbObj.parameter("@qty", data2);
rdbObj.execute();
}
rdbObj.close();
fileObj.close();
最初に、そのSQLでつかうパラメタを定義します。そしてSQLをコンパイルしておきます。
イメージとしてはプログラム言語で言うところの関数の定義、SQLならストアドプロシジャでしょうか。
※複雑ならストアドにしてしまってもいいんですけど、ストアドの言語仕様では加工が難しかったり、そもそもストアドの使用を許さない会社もあったりするので…
SQLを実行する際には、先にパラメタに値を割り当て、それからSQLを実行します。
この、SQL中にある“@xxx”のようなパラメタのことをプレースホルダといいます。いい加減な説明なのであとでちゃんと調べたほうが良いですよ。
SQLServerだと“@xxx”、Oracleだと“:xxx”かな。PostgreSQLとかは ? を使いますね。このときは名称ではなくn番目のパラメタ、として指定します。
すでにSQLはコンパイルされており、入力値によってSQLが改変されることはありません。
なので、特定文字のエスケープ処理をすることなく、SQLを実行することができます。
入力値の検証は無くせませんが、検証量は格段に減ります。
でも油断すると入力値の長さをチェックしていないといった、基本的なところを忘れがちになります。
領域のオーバーライドやオーバーフロー等が起きたりするのはまた別の問題を引き起こしますから。
こういう部分では手抜きをしないようにしてください。
Webブラウザへの出力をやるなら、出力中に怪しげなコードが含まれていてもそれを無効化するような仕掛けを入れます。
データベースの中身をWebブラウザに表示するとき、データベースのデータ中にJavaScriptのコードが入っているかもしれません。
xxコンパイラ用ライブラリ<JavaScript>document.href="http://xxxxx";</JavaScript>
こんな「文字列」がデータベースの項目「製品名」に入っていたら、表示中に別のサイトへ転送をかけることができるかもしれません。