MongoDBにおけるNoSQLインジェクション: 脆弱性の仕組みと対策

データベースを使ったシステムのセキュリティで、特に気をつけなければならないものの一つが、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: 評価式の書き換え


```javascript:app.js
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の使用をやめて、下記のようなパラメータ化されたクエリに置き換えることが挙げられます。
```javascript
const users = await User.find({
  username: username,
  password: password,
})
```


#### ケース2: オペレータ追加
```javascript:app.js
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}`
  });
});
```
このような実装になっている場合、通常下記のように実行されます。
```javascript
const users = await User.find({
  username: 'john',
  password: 'hoge',
});
```
このusernameやpasswordが文字列形ではなく、オブジェクト形式として受け取っていた場合、

```javascript
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