「演算子のインジェクション」と「SSJI」:NoSQLを使うなら知っておきたいセキュリティの話(1)(1/2 ページ)
ここ数年、大量データ処理時の高速性やデータ構造の柔軟性などから、「NoSQL」が注目を集めています。それと同時に、NoSQLを使うアプリケーションに対する攻撃手法も研究されるようになりました。この記事では、NoSQLを使ったアプリケーションの脆弱性と対策について解説します。
注目集める「NoSQL」
ここ数年、NoSQLと呼ばれる種類のデータベースが注目を集めています。NoSQLはSQL言語を使用しないデータベースの総称で、大量データ処理時の高速性やデータ構造の柔軟性などのメリットがあるため、従来のリレーショナルデータベース(RDB)を補完・代替するものとして、大規模なWebアプリケーションなどにおいてNoSQLを採用する事例が増えています。
このような新しい技術が普及し始めると、懸念されるのがセキュリティです。攻撃者は特にデータベースのデータの窃取や改ざんに大きな関心を持っているため、データベースとその一種であるNoSQLのセキュリティは重要と言えます。
データベースに対するセキュリティ上の脅威について振り返ってみると、Webアプリケーションに対するSQLインジェクション攻撃が猛威を振るってきたことが特筆されます。ここ10年ほど、SQLインジェクションによる被害が数え切れないほど多く発生してきました。RDB内の個人情報を窃取されたり、RDB内のWebコンテンツを改ざんされウイルスを埋め込まれたりするというものです。そしてその被害は今なお続いています。
それではNoSQLはどうなのでしょうか。
実際のところ、執筆時点においてはNoSQLに対する攻撃・被害の実例として知られているものは極めて少数にとどまっています。その主な要因は、NoSQLがまだ新しく、RDBほど広く使われていないことにあると思われます。その他の要因としては、現在は重要なデータをNoSQLに保存するケースが少ないこと、NoSQL側でSQLインジェクションの教訓を踏まえた一定のセキュリティ対策がされていること、データにアクセスするためのインターフェイスが製品ごとにバラバラであり攻撃がしづらいこと、なども挙げられます。
しかし、これはNoSQLを使うアプリケーションが無条件で安全であることを意味しません。ここ数年のNoSQLの普及に伴い、NoSQLはセキュリティ業界でも注目を浴びており、NoSQLを使うアプリケーションに対する攻撃手法も研究されてきています。その中で、従来のRDBとは異なる新しいタイプの攻撃方法も編み出されてきました。
【関連リンク】
NoSQL, no security? - Will Urbanski (DELL)
http://www.slideshare.net/wurbanski/nosql-no-security
このような背景から、本記事ではNoSQLに特化した攻撃方法や対策を説明します。取り上げるのはNoSQL製品自体のセキュリティではなく、NoSQLを使うアプリケーションの不備によって生じ得るインジェクション系の脆弱性です。
またNoSQLにもさまざまな製品がありますが、本記事で主に取り上げるのはMongoDBです。他にも、Cassandra、Redis、memcachedについて簡単に触れたいと思います。いずれもオープンソースのNoSQL製品です。
説明の中では、NoSQLにアクセスするプログラムの例をいくつか示します。PHPまたはJavaで書かれたものですが、簡単なものなので、これらの言語に詳しくない方でも直感的に理解できると思います。
それではまずは、MongoDBを使うアプリケーションについて、セキュリティ上の注意点などを説明します。
MongoDBを使うアプリケーションのセキュリティ
MongoDBは、ドキュメント指向型に分類されるオープンソースのNoSQLデータベースです。柔軟なデータ構造(スキーマレス)と、連想配列やJavaScriptを使った高度なクエリの処理が特徴です。執筆時点での最新バージョンは2.2.3で、本記事の内容はその挙動に基づきます。
まずは、MongoDBにアクセスするPHPプログラムのイメージをつかんでいただくために、サンプルプログラム(脆弱性がないプログラム)を示します。
// 登録するデータ(連想配列)を作成 $member = array('name' => 'MBSD', 'birthyear' => '1980', 'interests' => array('music', 'travel')); // データを登録 $db->members->insert($member); // データを検索しカーソルを取得 $cursor = $db->members->find(array('birthyear' => '1980'));
データベースへの接続処理などは省略しています。プログラム中の「members」($db->members)は、MongoDBにおいては「コレクション」と呼ばれるもので、RDBのテーブルに相当します。
プログラムでは最初にname、birthyear、interestsという3つのキーを持つ連想配列であるユーザー情報を定義し、それをMongoDBのコレクションに登録しています。次にそのコレクションからbirthyearが'1980'のユーザーを検索しています。登録・検索のいずれにおいても、引数としてPHPの連想配列を与えています。
MongoDBがスキーマレスと言われるのは、事前にコレクションのスキーマを定義する必要がなく、また同じコレクションに異なる構造を持つデータも保存できるためです。そのため、サービスの運用が始まった後であっても「name、birthyear、interestsに加えて、会員のemailも保存するようにする」といったことをデータベース側の変更なしに実現できます。
クエリのおおよそのイメージがつかめたところで、本題のMongoDBのセキュリティについての話に入ります。本記事では以下の4種類の脆弱性を取り上げます。
- 演算子のインジェクション
- SSJI(Server Side JavaScript Injection)
- JSON文字列へのインジェクション
- パラメータの追加
個別の脆弱性の内容を以下に見ていきます。
1. 演算子のインジェクション
MongoDBでは、クエリに与える連想配列のキーに演算子を含めることができます。不適切なプログラミングをしていると、攻撃者によってクエリ内に演算子を埋め込まれ、本来の意図とは異なるクエリに変えられてしまう危険性があります。
脆弱なアプリケーションの例
下記はMongoDBを使ってセッション管理を行うPHPのプログラムの一部です。
// CookieからセッションIDを取得する $sessionId = $_COOKIE['sessionId']; // セッションIDをキーにMongoDBのセッションコレクションを検索 // findOne()は検索結果のうち一行を返すメソッド $member = $db->sessions->findOne(array('sessionId' => $sessionId)); if ($member) { // アクセスしているユーザーはログイン済み $userId = $member['userId'];
このプログラムは、セッションIDをCookieから受け取り、それをキーにセッションコレクション(sessions)を検索します。検索結果が存在する場合には、ユーザーがログイン済みであるとみなします。そのあとの処理は省略していますが、その会員の情報を表示するなどの処理が続くものと考えてください。
正常なパラメータ(Cookie)は下記のような値であるとします。
正常パラメータ例: sessionId=3353f8f87cd6449abfbe320bc7c88446
本ケースにおける攻撃者の目標は、正しいセッションID(Cookie)をサーバに送ることなく、別の会員になりすますことです。
攻撃例
会員になりすますために、攻撃者は演算子を含むパラメータを送信します。具体的には下記のように操作したパラメータを送信します。
操作パラメータ例: sessionId[$ne]=x
PHPなどの言語には、配列/連想配列と解釈できる形式で書かれたリクエストパラメータを、プログラム内で配列/連想配列に展開する機能があります。この機能により、パラメータ操作を行った際にfindOneメソッドに与えられる連想配列は、下記のようにネストしたものになります。
array('sessionId' => array('$ne' => 'x'))
これを受け取るMongoDBは、「$ne」(Not Equal)というキーを否定演算子として扱うため、MongoDBは検索条件を「sessionId != 'x'」であると解釈します。セッションIDが「x」のユーザーは存在しないものと考えると、ログイン中の会員の全員が条件にマッチすることになります。従って、誰かは分からないものの、攻撃者はログイン済みの会員の1人としてログインした状態になってしまうでしょう。
この攻撃例では「$ne」演算子を使いましたが、この他にもMongoDBにはさまざまな演算子があり、攻撃に利用できるものは他にも多くあります。下記は「$regex」演算子を使って別の会員になりすます例です。
操作パラメータ例: sessionId[$regex]=^a
これにより、セッションIDの先頭が「a」である会員のいずれかとしてログインした状態になります。
対策
上記の問題の原因は、文字列型の入力データを期待しているにもかかわらず、連想配列などの意図しない形式の入力データを受け付けて使用していることにあります。対策はパラメータが文字列型であるかの型チェック、もしくは文字列型への型変換を行うことです。具体例は以下の通りです。
// 型チェックを行う場合 if(!is_string($sessionId)) { // エラー処理を行う } // 型変換を行う場合 $member = $db->sessions->findOne(array('sessionId' => (string)$sessionId));
なお、このような演算子挿入の問題は、パラメータのデータ型が固定的なプログラム言語においては発生しないと考えられます。筆者はPHPとRuby on Railsのプログラムで問題が発生し得ることを確認しました。
Copyright © ITmedia, Inc. All Rights Reserved.