2014年7月7日月曜日

JSON SQL Injection、PHPならJSONなしでもできるよ

DeNAの奥さんと、はるぷさんがJSON SQL Injectionについて公表されています。
上記の記事は、主にPerlスクリプトがJSONデータを受け取るシナリオで説明されています。もちろん、この組み合わせに限定したはなしではないわけで、それではPHPではどうだろうと思い調べてみました。

JSON SQL Injectionとは

以下、はるぷさんの「不正なJSONデータによる…」にしたがってJSON SQL Injectionについて説明します。
Perl向けのSQLジェネレータの一つであるSQL::Makerにおいて、以下のスクリプトを想定します。
my ($sql, @bind) = $builder->select(
    "table_name", ["*"],
    {"name"=>$user_name});
ここで、$user_nameとして 'yamada' を与えると、以下のSQL文が生成されます。
SQL文: SELECT * FROM `table_name` WHERE (`name` = ?)
変数: yamada
$user_nameが外部からJSON形式で渡ってくる場合(APIなどのように)を想定すると、ここの部分のJSONデータが {"key":"value"} となっていると、SQL::Makerは、keyをWHERE句の比較演算子とみなし、以下のSQL文を生成します。
SQL文: SELECT * FROM `table_name` WHERE (`name` KEY ?)
変数: value
ここで、keyの部分は任意の文字列を書くことができ、それが *そのまま* SQL文に流し込まれます。この仕様を悪用すると、SQL文を本来意図しない形に改変できてしまいます。これがJSON SQL Injectionです。


PHPではどうか?

それでは、我らがPHPではどうでしょうか。
@memememomoさんがSQL::MakerをPHPに移植されておられまして(参照)、これを使うと、JSON SQL Injectionが再現できます。
$builder = new SQL_Maker(array('driver' => 'mysql'));
list($sql, $binds) = $builder->select('table_name', array('*'), array('name' => $user_name));
ここで、$user_nameが'yamada'の場合、以下のようにPerl版とまったく同じ結果になります。
SQL文: SELECT * FROM `table_name` WHERE (`name` = ?)
変数: yamada
次に、$user_name = array('key' => 'value'); とすると、生成されるSQL文は下記のとおりです。
SELECT * FROM `table_name` WHERE (`name` key ?)
すなわち、JSONのキーとして指定した値が *そのまま* SQL文に注入されます。JSON SQL InjectionがPHPでも再現出来ました。
ここでは、PHP版のSQL::Makerを題材として使用させていただきましたが、後述するように、他にも同様の挙動をしめすSQLジェネレータを確認しています。

PHPの場合はJSONは必要ない

しかし、上記のスクリプトにおいて、SQLインジェクションを起こすためには、入力値がJSON形式である必要はありません。$user_nameが連想配列(文字列をキーとした配列)であれば良いわけですので、PHPの場合であれば、以下のようにHTMLフォームからでも指定は可能です。
先のスクリプトで、$user_name = $_GET['user_name'] としていた場合、以下のクエリ文字列でスクリプトを呼び出すと…
http://example.jp/query.php?user_name[key]=value
生成されるSQL文は下記となります。
SELECT * FROM `table_name` WHERE (`name` key ?)
このため、keyの箇所に任意のSQL断片を指定することにより、SQLインジェクション攻撃が可能となることを確認いたしました。一例を挙げます。
http://example.jp/query.php?user_name[>''+or+1%3d1)%23]=value
生成されるSQL文は下記となります。
SELECT * FROM `table_name` WHERE (`name` >'' or 1=1)# ?)
PHPの場合、JSONを使わなくてもJSON SQL Injectionができてしまう(*1)ことになり、該当するSQLジェネレータを使っている場合、広範囲のアプリケーションが脆弱になる可能性があります。

*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));
この際の実行結果は下記のように例外が発生します。
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
strictモードが使えない場合は、条件設定に与える引数の型チェックを行う方法があります。
  $user_name = $_GET['user_name'];
  if (! is_string($user_name)) {
    # エラー処理
    exit;
  }

他のSQLジェネレータの状況

SQL::AbstractのPHPへの移植版についても同種の問題があることが分かっています。作者に連絡をとったところ、古く個人的なプロジェクトであり、かつ開発者自身の使い方では strict モードに移行すると過去のスクリプトが動かなくなると言うことで、strictモードの対応は今のところないということでした。元々、ライブラリ側の脆弱性とまでは言えない問題ですので、これは仕方ないと考えます。呼び出し側での対応をするのがよいでしょう。

FluentPDOの場合、任意のSQL断片を注入することはできないようですが、パラメータとして配列を与えることによって、等号ではなく IN 演算子を使うものに変更はできるようです。以下のスクリプトで説明します。
$pdo = new PDO("mysql:dbname=...
$fpdo = new FluentPDO($pdo);  // FluentPDOオブジェクトの生成
$query = $fpdo->from('user')->where('id', $_GET['id']);
まず通常のケースとして、クエリ文字列を ?id=yamada とすると、生成されるSQL文は以下となります。yamadaはバインドする値として指定されます。
SELECT user.* FROM user WHERE id = ?
次に、?id[]=yamada&id[]=sato とすると、生成されるSQL文は以下の通りです。
SELECT user.* FROM user WHERE id IN ('yamada', 'sato')
これにより、特定IDの存在の有無を確認するようなケースでは、一度に多数の候補を試すことができることから、少ないリクエストでの探索が可能になります。


まとめ

SQLジェネレータが生成するSQL文について、PHPを使う場合は、JSONを用いていない場合でも、JSON SQL Injectionと類似の問題が発生する可能性があることを紹介しました。ライブラリの特性・仕様を理解した上で、正しい使い方により、脆弱性の混入を防ぎましょう。

また、私1人の調査には限界がありますので、類似の問題を見つけた方は教えて頂けると幸いです。

フォロワー

ブログ アーカイブ