NoSQLインジェクション:論理的脆弱性の深淵と「型」の暴走
SQLインジェクションが構文解析器(パーサー)を欺く「文字列の混入」であるならば、NoSQLインジェクションは、JSONというデータ構造そのものを「意図しない型」で汚染する「意味論的なハック」だ。
MongoDBのようなドキュメント指向データベースにおいて、$gt や $ne といったクエリ演算子は、本来は開発者が制御すべきロジックの断片である。しかし、アプリケーションがユーザー入力を生のオブジェクトとしてクエリに渡した瞬間、境界は崩壊する。今回は、この「動的な型混入」がなぜ防げないのか、そしてアーキテクトがどこで防衛線を引くべきかを、低レイヤの視点から掘り下げる。
1. 脆弱性の解剖:BSONの構造と型混入のメカニズム
MongoDBが採用しているバイナリ形式の BSON は、データの型情報(Type ID)を値の直前に保持している。攻撃者が行うのは、単純な文字列の挿入ではなく、{ "username": { "$gt": "" } } のような「演算子を含むオブジェクト」をJSONとして注入し、パーサーにそれを「検索条件」ではなく「データの一部」として誤認させることだ。
Node.jsの Express アプリケーションでよくある過ちを見てほしい。
// 脆弱な実装例
// クエリパラメータをそのままクエリ条件に代入している
app.post('/login', (req, res) => {
const query = {
username: req.body.username, // ここにオブジェクトを注入されると危険
password: req.body.password
};
db.collection('users').findOne(query, (err, user) => {
// ...
});
});
このコードに対し、攻撃者は Content-Type: application/json で以下のペイロードを送る。
{
"username": {"$gt": ""},
"password": {"$gt": ""}
}
データベースエンジンに到達する時点で、username は「特定の文字列」ではなく「空文字よりも大きい全ての文字列」という評価式に変換される。認証ロジックは「最初のレコードを返す」という挙動になり、パスワードを知らずともログインが成立する。これは単なるコードの不備ではない。「型を信頼する」という設計思想そのものの欠陥なのだ。
2. 根本的な防衛戦略:バリデーションから「スキーマ強制」へ
表層的な sanitizer や正規表現によるフィルタリングは、攻撃者がBSONの仕様やエンコーディングを巧みに操る前では無力だ。真のアーキテクトは、データがデータベースのクエリレイヤーに触れる前に、厳格な「型」を適用する。
推奨アプローチ:Mongooseによるスキーマ強制
MongooseのようなODM(Object Data Modeling)を使用する場合、入力値を定義済みのスキーマにキャストし、演算子を含むオブジェクトを排除する。
const mongoose = require('mongoose');
// スキーマで型を厳密に定義する
const UserSchema = new mongoose.Schema({
username: { type: String, required: true },
password: { type: String, required: true }
});
// モデル生成時に型キャストを適用
const User = mongoose.model('User', UserSchema);
// クエリ実行前にバリデーションとキャストを行う
async function login(username, password) {
// 攻撃者がオブジェクトを送っても、String型に変換されるため演算子は無効化される
const query = {
username: String(username),
password: String(password)
};
return await User.findOne(query);
}
なぜこれが有効なのか?
ここで重要なのは String() によるキャストだ。もし req.body.username が {"$gt": ""} というオブジェクトであっても、文字列に強制変換されることで、"[object Object]" という単なる文字列としてデータベースに問い合わせるようになる。演算子としての意味は完全に消失する。
3. 防御のアーキテクチャ:生成AI時代におけるガードレイル
現代のシステムでは、LLMを介したクエリ構築も増えている。ここで問題になるのは「プロンプトインジェクションによるクエリ汚染」だ。LLMにデータベース操作を委ねる場合、以下のようなガードレイルをプロキシ層に設けるのが鉄則である。
1. 抽象化レイヤーの分離: アプリケーション層から直接データベースの演算子を許可してはならない。APIインターフェースにおいて、"operator": "gt" のような文字列を受け取り、バックエンドで許可リスト(Allowlist)に基づいて query オブジェクトを再構築する。
2. パケットインスペクション: データベースへの通信を監視するネットワークセキュリティ機器やサイドカー(Envoy等)で、$gt, $ne, $regex といった演算子が異常な頻度やコンテキストで出現していないかをモニタリングする。
3. 最小権限の原則: 認証処理を行うためのDBユーザーは、クエリ演算子を自由に使えない読み取り専用かつ、特定のインデックスを介したアクセスのみに制限する。
4. 最後に:セキュリティは「構造」への信頼から始まる
NoSQLインジェクションを防ぐ鍵は、「入力されたJSONをそのままロジックの部品として信じない」という疑念をコードの隅々に浸透させることにある。
私たちは、Webアプリケーションの脆弱性を探る際、常に「開発者が想定した型」と「実際に送られてくるバイナリ構造」の隙間を突く。その隙間を埋める唯一の方法は、フレームワークの魔法を信じることではなく、「データがデータベースに触れる直前で、一度『型』の純潔性チェックを行う」という泥臭い実装だ。
テックリードとして、もしあなたのプロジェクトでクエリが動的に構築されているなら、今すぐそのコードを監査せよ。それは、データベースの全権を攻撃者に委ねているのと同義かもしれない。
コメント