データベースを使ったシステムのセキュリティで、特に気をつけなければならないものの一つが、SQLインジェクションです。SQLインジェクションとは、外部からの意図しない入力により不正なSQL文を実行されてしまう脆弱性です。SQLで操作するデータベースにおいて、ユーザーからの入力を適切に検証・エスケープせずにSQLクエリに埋め込むような実装になっていた場合に発生します。
NoSQLデータベースを使用すればSQLインジェクションは発生しませんが、その場合NoSQLインジェクションに注意する必要があります。
この記事ではMongoDBを使ったシステムの実装の例を元に、NoSQLインジェクションを解説していきます。
NoSQLとSQLの違い
SQLとはStructured Query Languageの略で、リレーショナルデータベースのデータを操作・取得するための標準的なプログラミング言語です。
それに対してNoSQLとは、リレーショナルデータベースとは異なるデータモデルを採用したデータベースの総称を言います。MongoDBやRedisのようなデータベースがNoSQLに分類されます。
SQLはプログラミング言語なのに対し、NoSQLはSQLを使わない非リレーショナルデータベースのことを言います。
NoSQLインジェクションとは?
NoSQLインジェクションとは、NoSQLデータベースで発生するインジェクションの脆弱性です。NoSQLではデータの操作にSQL文を使わずに、独自のクエリ言語、独自のAPIを使用します。データベースの種類ごとに違う方法で操作するため、インジェクションの発生パターンも一律ではありません。
とはいえ、いくつか共通の対策もあり、
- パラメータ化されたクエリを使用する
- リクエストパラメータが不正な形式だった場合は処理を行わない
などを行うことで、リスクを低減させることが可能です。
脆弱な実装の例
使用する環境
- プログラミング言語
- Node.js
- データベース
- MongoDB
- ライブラリ
- mongoose
- express
簡易なWebサービスのログイン用APIを例にとって解説していきます。データベース操作はMongoDB操作用のライブラリ「mongoose」を使用します。
Userモデル
username: String
password: String
エンドポイント: POST /login
送信するJSON
{
username: 'john',
password: 'hoge',
}
ケース1: 評価式の書き換え
app.post('/login', async (req, res) => {
const username = req.body.username;
const password = req.body.password;
const users = await User.find({
$where: `this.username === '${username}' && this.password === '${password}'`
});
if (users.length === 0) {
res.json({
message: 'Failed to log in.'
});
return;
}
res.json({
message: `Successfully logged in as ${users[0].username}`
});
});
$where: `this.username === '${username}' && this.password === '${password}'`
このような実装の場合、通常下記のように実行されます。
$where: `this.username === 'john' && this.password === 'hoge'`
このusernameやpasswordにシングルクォート文字列が含まれていた場合、新たな条件式を追加できます。
$where: `this.username === 'john' && this.password === '' || this.username === 'admin'`
このような文字列を挿入すると、パスワードなしで他のユーザーとしてログインができてしまいます。
対策方法としては、$whereの使用をやめて、下記のようなパラメータ化されたクエリに置き換えることが挙げられます。
const users = await User.find({
username: username,
password: password,
})
ケース2: オペレータ追加
app.post('/login', async (req, res) => {
const username = req.body.username;
const password = req.body.password;
const users = await User.find({
username: username,
password: password,
});
if (users.length === 0) {
res.json({
message: 'Failed to log in.'
});
return;
}
res.json({
message: `Successfully logged in as ${users[0].username}`
});
});
このような実装になっている場合、通常下記のように実行されます。
const users = await User.find({
username: 'john',
password: 'hoge',
});
このusernameやpasswordが文字列形ではなく、オブジェクト形式として受け取っていた場合、
const users = await User.find({
username: 'john',
password: {
$ne: 'dummy'
},
});
$neはMongoDB用の比較用のオペレータです。Not Equal(一致しない)の意味となるため、パスワードが一致しなくてもログインが成立してしまいます。
対策としては下記のような修正が有効です。
- パラメータの型がオブジェクト型ではないことを確認する
- パラメータの型が適切な型である(例では文字列型)ことを確認する
まとめ
MongoDBなどのNoSQLデータベースを使用する場合、そのデータベースのクエリ言語やAPIでインジェクションが起こる可能性がある箇所を洗い出して、適切に入力チェックをする必要があります。
MongoDBの場合、具体的には下記の方法で防ぐことが出来ます。
- パラメータ化されたクエリの使用($whereを使用しない)
- パラメータの型のバリデーション
おわりに
AeyeScanはSaaS型のWebアプリケーション脆弱性診断プラットフォームです。かんたんに高精度なWeb診断を実施することができることから、Webアプリ診断の内製化ツールとして多くの企業様にご活用いただいております。
今回解説したMongoDBのNoSQLインジェクションも検査することができますので、ご興味ある方はぜひトライアルにてお試しください。
AeyeScanの詳細はこちら: https://www.aeyescan.jp