技術者ブログ
クラウド型WAF「Scutum(スキュータム)」の開発者/エンジニアによるブログです。
金床“Kanatoko”をはじめとする株式会社ビットフォレストの技術チームが、“WAFを支える技術”をテーマに幅広く、不定期に更新中!
PHP環境で発生するMongoDB Request Injection攻撃
はじめに
SaaS型WAF「Scutum(スキュータム)」では、最近バックエンドのDBにMongoDBの導入を進めています。私も個人的にPHPのフレームワークであるCakePHP向けにMongoDBを扱うプラグインCakePHP-MongoDB Datasourceを開発し、公開しています。
https://github.com/ichikaway/cakephp-mongodb/
今回は、PHPからMongoDBを扱う際の注意点として、RequestInjection攻撃に関して書きたいと思います。この問題は、phpマニュアルのMongoドライバーの章でも扱われています。
この記事では、はじめにPHPでMongoを操作する方法、PHPの基本を書き、最後にInjection攻撃の仕組みと対応方法を書きます。
PHPでMongoDBを扱うには
PHPでMongoDBを操作する場合、10gen社が開発しているpecl mongoドライバーを利用します。データをusersコレクション(Table)から取得する場合は、下記のようにします。
$mongo = new Mongo('mongodb://localhost:27017'); $collection = $mongo->selectDB("foo")->selectCollection("users"); $cursor = $collection->find(array('status' => array('$gte' => 1)));
findの検索条件は、連想配列で記述します。MongoDBのオペレータは配列のKeyに文字列として '$gt' というように記述します。ここでは、usersコレクションのstatusカラムに、1以上がセットされているユーザを取得する条件がセットされています。
PHPの基本
PHPではGet/Postリクエストで送信したデータが$_GET/$_POSTというグローバル変数に配列として格納されます。例えば、
index.php?foo=bar というURLにアクセスすると、$_GET['foo']にbarという文字列が入ります。
さらに、クエリストリングのKeyを配列のように記述すると、サーバ側では配列として格納されます。
例えば、 index.php?foo[foo2][foo3]=bar
というURLにアクセスすると、$_GET['foo']['foo2']['foo3']にbarという文字列が入ります。
PHPへのMongoDB Request Injection攻撃
ここで説明するInjection攻撃は、外部からMongoDBオペレータを注入できるというものです。例えば、usersコレクションから該当ユーザをID/PWで引き当てる場合、
$cursor = $collection->find(array( "username" => $_GET['username'], "passwd" => $_GET['passwd'] ));
というような処理があったとしましょう。
そのphpスクリプトに対して、下記のようなリクエストを送ると、adminユーザが引き当てられてしまいます。
login.php?username=admin&passwd[$ne]=1
これは「PHPの基本」の章で書いたように、PHPはKeyの記述によってそのまま配列に展開する機能があるためです。実際のプログラムでは下記のように実行されてしまいます。
$cursor = $collection->find(array( "username" => "admin", "passwd" => array("$ne" => 1) ));
PHPが送信データのKeyの文字列を解析して配列に展開してしまうことと、MongoDBのオペレータが$gtのような文字列で指定できることが組み合わさり、外部からMongoDBオペレータを注入できるというのが、この問題の原因です。
※今回はGETで説明していますが、POSTも同様の問題があります。
対応方法
この問題を回避するために、文字列で値を指定する箇所は下記のようにします。配列で値が渡ってきたとしても、username/passwdの値はArrayという文字列になるため問題を回避できます。
$cursor = $collection->find(array( "username" => (string)$_GET['username'], "passwd" => (string)$_GET['passwd'] ));
上記の対応方法は、アプリケーションを実装する上で常に注意する必要があります。面倒な場合は$_POST/$_GETなどのユーザ入力値を全てチェックして、Keyに不正な文字(ここでは$文字)が含まれている場合は、エラーやunset()で値を消すという方法もあります。この方法は毎回ユーザ入力データをスキャンするため多少負荷がかかりますのでご注意ください。
この方法を手軽に実現するために、私が開発している下記のツールがあります。
https://github.com/ichikaway/SecurePHP
このツールをダウンロードし、下記のようにbootstrap.phpをアプリケーションの最初に読み込むと、Keyの文字チェック、値のnullバイトチェックなどを行います。
require_once(YOUR_LIB_PATH . 'SecurePHP/config/bootstrap.php');
配列のKeyは、英数字と:_./-のみを許すようにしていますので、$が含まれていると値がunsetされます。これは$allowKeyNameRegexというクラス変数で定義しているので、アプリケーションにフィットしなければ変更してご利用ください。
結論
PHPでMongoDBを扱うのはPeclMongoドライバーのおかげで非常に簡単です。ただし、このような言語の特性によって起こる問題もあるため、常に最新のPHPマニュアルを参照すると良いと思います。英語ドキュメントが一番更新が早いため、そちらをチェックすることをお勧めします。
参考資料
「Do I Have to Worry About SQL Injection」
http://www.mongodb.org/display/DOCS/Do+I+Have+to+Worry+About+SQL+Injection
「PHPマニュアル」
http://www.php.net/manual/en/mongo.manual.php#mongo.security
「Mongodb is vulnerable to SQL injection in PHP at least」
http://www.idontplaydarts.com/2010/07/mongodb-is-vulnerable-to-sql-injection-in-php-at-least/