上記の記事は、主にPerlスクリプトがJSONデータを受け取るシナリオで説明されています。もちろん、この組み合わせに限定したはなしではないわけで、それではPHPではどうだろうと思い調べてみました。
JSON SQL Injectionとは
以下、はるぷさんの「不正なJSONデータによる…」にしたがってJSON SQL Injectionについて説明します。Perl向けのSQLジェネレータの一つであるSQL::Makerにおいて、以下のスクリプトを想定します。
ここで、$user_nameとして 'yamada' を与えると、以下のSQL文が生成されます。my ($sql, @bind) = $builder->select( "table_name", ["*"], {"name"=>$user_name});
$user_nameが外部からJSON形式で渡ってくる場合(APIなどのように)を想定すると、ここの部分のJSONデータが {"key":"value"} となっていると、SQL::Makerは、keyをWHERE句の比較演算子とみなし、以下のSQL文を生成します。SQL文: SELECT * FROM `table_name` WHERE (`name` = ?) 変数: yamada
ここで、keyの部分は任意の文字列を書くことができ、それが *そのまま* SQL文に流し込まれます。この仕様を悪用すると、SQL文を本来意図しない形に改変できてしまいます。これがJSON SQL Injectionです。SQL文: SELECT * FROM `table_name` WHERE (`name` KEY ?) 変数: value
PHPではどうか?
それでは、我らがPHPではどうでしょうか。@memememomoさんがSQL::MakerをPHPに移植されておられまして(参照)、これを使うと、JSON SQL Injectionが再現できます。
ここで、$user_nameが'yamada'の場合、以下のようにPerl版とまったく同じ結果になります。$builder = new SQL_Maker(array('driver' => 'mysql')); list($sql, $binds) = $builder->select('table_name', array('*'), array('name' => $user_name));
次に、$user_name = array('key' => 'value'); とすると、生成されるSQL文は下記のとおりです。SQL文: SELECT * FROM `table_name` WHERE (`name` = ?) 変数: yamada
すなわち、JSONのキーとして指定した値が *そのまま* SQL文に注入されます。JSON SQL InjectionがPHPでも再現出来ました。SELECT * FROM `table_name` WHERE (`name` key ?)
ここでは、PHP版のSQL::Makerを題材として使用させていただきましたが、後述するように、他にも同様の挙動をしめすSQLジェネレータを確認しています。
PHPの場合はJSONは必要ない
しかし、上記のスクリプトにおいて、SQLインジェクションを起こすためには、入力値がJSON形式である必要はありません。$user_nameが連想配列(文字列をキーとした配列)であれば良いわけですので、PHPの場合であれば、以下のようにHTMLフォームからでも指定は可能です。先のスクリプトで、$user_name = $_GET['user_name'] としていた場合、以下のクエリ文字列でスクリプトを呼び出すと…
生成されるSQL文は下記となります。http://example.jp/query.php?user_name[key]=value
このため、keyの箇所に任意のSQL断片を指定することにより、SQLインジェクション攻撃が可能となることを確認いたしました。一例を挙げます。SELECT * FROM `table_name` WHERE (`name` key ?)
生成されるSQL文は下記となります。http://example.jp/query.php?user_name[>''+or+1%3d1)%23]=value
PHPの場合、JSONを使わなくてもJSON SQL Injectionができてしまう(*1)ことになり、該当するSQLジェネレータを使っている場合、広範囲のアプリケーションが脆弱になる可能性があります。SELECT * FROM `table_name` WHERE (`name` >'' or 1=1)# ?)
*1: この場合、JSON SQL Injectionという呼称は適切でないと考えられます。
対策
PHP版のSQL::Makerについては、対策版が公開されています。本家Perl版と同様に、strictモードが導入されました。memememomoさん、対応ありがとうございます。これは以下のように使用します。
$builder = new SQL_Maker(array('driver' => 'mysql', 'strict' => 1));
$user_name = array('key' => 'value');
list($sql, $binds) = $builder->select('table_name', array('*'), array('name' => $user_name));
この際の実行結果は下記のように例外が発生します。strictモードが使えない場合は、条件設定に与える引数の型チェックを行う方法があります。PHP Fatal error: Uncaught exception 'Exception' with message 'cannot pass in a ref as argument in strict mode' in /home/ockeghem/php-SQL-Maker-master/lib/SQL/Maker/Condition.php:52
$user_name = $_GET['user_name']; if (! is_string($user_name)) { # エラー処理 exit; }
他のSQLジェネレータの状況
SQL::AbstractのPHPへの移植版についても同種の問題があることが分かっています。作者に連絡をとったところ、古く個人的なプロジェクトであり、かつ開発者自身の使い方では strict モードに移行すると過去のスクリプトが動かなくなると言うことで、strictモードの対応は今のところないということでした。元々、ライブラリ側の脆弱性とまでは言えない問題ですので、これは仕方ないと考えます。呼び出し側での対応をするのがよいでしょう。FluentPDOの場合、任意のSQL断片を注入することはできないようですが、パラメータとして配列を与えることによって、等号ではなく IN 演算子を使うものに変更はできるようです。以下のスクリプトで説明します。
まず通常のケースとして、クエリ文字列を ?id=yamada とすると、生成されるSQL文は以下となります。yamadaはバインドする値として指定されます。$pdo = new PDO("mysql:dbname=... $fpdo = new FluentPDO($pdo); // FluentPDOオブジェクトの生成 $query = $fpdo->from('user')->where('id', $_GET['id']);
次に、?id[]=yamada&id[]=sato とすると、生成されるSQL文は以下の通りです。SELECT user.* FROM user WHERE id = ?
これにより、特定IDの存在の有無を確認するようなケースでは、一度に多数の候補を試すことができることから、少ないリクエストでの探索が可能になります。SELECT user.* FROM user WHERE id IN ('yamada', 'sato')
まとめ
SQLジェネレータが生成するSQL文について、PHPを使う場合は、JSONを用いていない場合でも、JSON SQL Injectionと類似の問題が発生する可能性があることを紹介しました。ライブラリの特性・仕様を理解した上で、正しい使い方により、脆弱性の混入を防ぎましょう。また、私1人の調査には限界がありますので、類似の問題を見つけた方は教えて頂けると幸いです。
0 件のコメント:
コメントを投稿