CyberFix Note
セキュアコーディング

NoSQLインジェクションの仕組みと対策。クエリ演算子の注入で認証回避やデータ抽出に至る欠陥を塞ぐ

対象の目安: WebアプリとAPIの開発者 / 実務

ソウ攻撃・脆弱性リサーチ担当
・ 約12分で読めます
NoSQLインジェクションの仕組みと対策。クエリ演算子の注入で認証回避やデータ抽出に至る欠陥を塞ぐ

MongoDBに代表されるドキュメント指向のデータストアは、SQLのような固定の問い合わせ言語ではなく、フィールドと条件をJSONのオブジェクトとして表現してデータを検索します。この検索条件に、利用者から受け取った値を型も確かめずそのまま組み込むと、攻撃者が条件の構造そのものを差し替えられる余地が生まれます。これがNoSQLインジェクションです。攻撃が成立すると、ログイン条件をすり抜けて他人になりすます認証回避、保存されたデータの1文字ずつの抽出、環境によってはサーバー側でのJavaScript評価による高負荷や情報漏えいまで、被害が広がります。

やっかいなのは、SQLインジェクション対策として身についた「危険な文字をエスケープする」という発想が、そのままでは効きにくい点です。NoSQLインジェクションの多くは文字列の連結ではなく、本来は文字列であるべき値の位置に、演算子を含んだオブジェクトが差し込まれることで起きます。この記事は、WebアプリとAPIの開発者に向けて、なぜオブジェクトや演算子の注入が成立するのかを機構レベルで整理し、型検証と安全なクエリAPIを軸にした根本対策までをまとめます。攻撃コードは防御を理解するための最小限にとどめます。

NoSQLインジェクションとは何か

NoSQLインジェクションは、アプリケーションが組み立てるデータストアへの問い合わせに、利用者の入力が命令(検索条件)の一部として紛れ込む欠陥です。MITREのCWE-943は、この種の欠陥を「データストアへの問い合わせを生成する際に、問い合わせの意図した論理を書き換え得る特殊要素を適切に無害化しない」ものと定義しています。NoSQLインジェクションには専用のCWEがなく、この親であるCWE-943そのものに対応づけられます。一方でSQLインジェクション(CWE-89)はCWE-943の子として整理されています。対象がMongoDBやCassandra、ElasticSearch、Redisなどに変わっても、命令とデータを混ぜてしまうという根は共通です。

PortSwiggerのWeb Security Academyは、NoSQLインジェクションを二つの型に整理しています。一つは問い合わせの構文そのものを壊して任意のペイロードを差し込む構文インジェクション、もう一つはNoSQLのクエリ演算子を使って問い合わせの挙動を操る演算子インジェクションです。実務で頻繁に問題になるのは後者で、たとえば {"username":{"$ne":"invalid"}} のように、文字列を期待している位置へ演算子を含んだオブジェクトを送り込みます。

MITREのCWE-943は、データストアへの問い合わせを生成する際に、その論理を書き換え得る特殊要素を適切に無害化しない欠陥と定義しています。SQLインジェクション(CWE-89)はCWE-943の子として位置づけられ、CWE-943はMongoDBなどNoSQLへのインジェクションも含みます。

SQLインジェクションと何が違うのか

SQLインジェクションは、WHERE name = '入力値' のような文字列にシングルクォートや論理演算子を混ぜ、命令とデータの境界を壊すことで成立します。対してNoSQLインジェクションでは、条件がJSONのオブジェクトとして表現されるため、境界を壊す手段が文字列だけではありません。リクエストで受け取ったパラメータが文字列ではなくオブジェクトとして届いたとき、それがそのまま検索条件のオブジェクトに組み込まれると、キーに書かれた $ne$gt が演算子として解釈されてしまいます。

この違いは対策にも直結します。シングルクォートをエスケープする発想は、値が文字列であることを前提にした防御です。NoSQLでは値がオブジェクトに化けることが問題の中心なので、文字のエスケープではなく、受け取った値が本当に文字列なのかという型の検証が先に立ちます。命令とデータを分離するという原理はSQLインジェクションと同じですが、守るべき境界が「文字列の中」から「値の型と構造」へ移ると捉えると見通しが良くなります。

あわせて読みたい

SQLインジェクションとは何か。仕組み・攻撃手法・影響・対策を原理から徹底解説

なぜ演算子やオブジェクトの注入が起きるのか

NoSQLインジェクションが起きるのは、リクエストで受け取った値をアプリが型を確かめないままクエリオブジェクトへ組み込み、本来は文字列であるべき位置に攻撃者が渡した演算子付きオブジェクトが検索条件として解釈されるためです。多くのWebフレームワークは、application/json の本文や、user[$ne]=1 のような形式のクエリ文字列を、自動でネストしたオブジェクトへ展開します。開発者が文字列のつもりで受け取ったつもりでも、実際にはオブジェクトが渡ってくる経路が用意されているわけです。

次のコードは、受け取った値の型を確かめずにそのまま検索条件へ渡しており、脆弱です。

// 脆弱: 受け取った値の型を確かめずクエリオブジェクトへ渡している
const user = await db.collection("users").findOne({
  username: req.body.username,
  password: req.body.password,
});

req.body.username が文字列 "alice" なら意図どおり動きますが、{"$ne": null} というオブジェクトが届くと、条件は username が null 以外 という意味に化けます。開発者が書いた命令の骨組みに、攻撃者がデータの皮をかぶった演算子を差し込んだ状態です。

典型的な攻撃の形

代表的な三つの形を、防御を理解する目的で最小限に示します。いずれも共通するのは、文字列を期待する位置へオブジェクトや演算子を送り込む点です。

一つ目は認証回避です。ログイン処理で利用者名とパスワードの両方が演算子を受け付けてしまうと、{"username":{"$ne":"invalid"},"password":{"$ne":"invalid"}} のような本文で、どちらも特定値以外という条件になり、最初に一致した利用者としてログインが通ってしまいます。二つ目はデータ抽出で、$regex を使って {"password":{"$regex":"^a"}} のように先頭文字を推測し、一致の有無を手がかりに1文字ずつ秘密の値を復元します。三つ目はサーバー側でのJavaScript評価で、$where に渡した式が評価される性質を悪用し、重い処理を走らせてサービスを妨害したり、条件判定を通じて情報を漏らしたりします。

PortSwiggerのWeb Security Academyは、演算子インジェクションを、$ne、$gt、$regex、$whereなどのクエリ演算子をネストしたオブジェクトとして注入し問い合わせを操る手法として整理しています。両フィールドが演算子を受け付ける場合に認証回避が成立し、$regexや$whereを使うと値を1文字ずつ復元できると説明しています。

注意

これらの手法の検証は、必ず自分が管理する環境、または明示的に許可を得た対象に対してのみ行ってください。許可のないシステムへの調査や攻撃は、不正アクセス禁止法をはじめとする法令に抵触します。本記事の内容は、自社システムの防御を確認する目的で理解してください。

根本対策の組み立て方

対策の軸は、利用者の入力を検索条件のオブジェクトへ混ぜる前に型と構造を確定させ、演算子の注入経路を断つことです。次の順で組み立てます。

  1. 1

    入力の型とスキーマを検証する

    文字列を期待する項目には、検索条件へ渡す前に文字列であることを確かめ、オブジェクトや配列が届いたら拒否します。JSON SchemaやZod、express-validatorのようなスキーマ検証を入口に置き、型と長さ、書式を満たさない入力を早期にはじきます。値がオブジェクトへ化ける経路を塞ぐことが、演算子インジェクションへの一次防御になります。

  2. 2

    $始まりのキーや演算子を無害化して拒否する

    検索条件へ渡すデータのキーに、$で始まる名前やドットを含む名前があれば拒否します。express-mongo-sanitizeのようなミドルウェアで一括除去する方法もありますが、除去より、想定しないキーを含む入力そのものを拒否する検証を優先します。OWASPは、$whereや$regex、$exprといったクライアント由来の演算子を、明確に必要で検証済みの場合を除いて許可しないことを推奨しています。

  3. 3

    ドライバやODMの安全なクエリAPIでフィールドを固定する

    問い合わせは文字列として組み立てず、ドライバのクエリオブジェクトを使い、各フィールドに検証済みの値を明示的に割り当てます。ただしMongooseのようなオブジェクトドキュメントマッパーは、$neや$regexのような演算子を認識してそのオペランドだけを型変換するため、スキーマの型を固定しただけでは { "$ne": null } のような演算子オブジェクトの注入までは弾けません。Mongoose 6以降のsanitizeFilterを有効にして、$で始まるキーを持つ入れ子を$eqで包むか、前段で演算子オブジェクトを拒否することで、演算子として解釈される余地をなくします。値をキャストしてから条件へ入れる設計にします。

  4. 4

    $whereなどサーバー側JavaScriptを使わない

    $whereやmapReduce、evalのようにサーバー上でJavaScriptを評価する機能は使わず、標準のクエリ演算子や$exprで同じ条件を表現します。MongoDBは8.0以降、$whereを含むサーバー側JavaScript関数を非推奨とし、実行時に警告を記録します。可能なら設定でサーバー側スクリプティング自体を無効にし、評価経路をなくします。

  5. 5

    最小権限のアカウントで接続する

    アプリが使うデータベースユーザーの権限を必要最小限に絞り、管理操作やスクリプティングの権限を与えません。万一いずれかの層をすり抜けられても、権限がなければ被害を限定できます。あわせて接続の暗号化を有効にし、データストアを外部から直接到達できない内部ネットワークに置きます。

OWASPのNoSQL Security Cheat Sheetは、問い合わせ文字列を組み立てるのではなくドライバのクエリオブジェクトを使うこと、$whereや$regexなどクライアント由来の演算子を検証済みで必要な場合を除き許可しないこと、キーの$を不要な箇所で拒否すること、サービスアカウントに最小権限を適用すること、信頼できないデータへevalを使わずサーバー側スクリプティングを無効化すること、通信にTLSを強制することを挙げています。

型検証と演算子の拒否は、より広い入力検証とエスケープの土台の上に位置づけられます。役割分担と限界は次の記事で整理しています。

あわせて読みたい

入力バリデーションと出力エスケープの原則。入口で検証し、出口で文脈別にエスケープする

まとめ

NoSQLインジェクションは、信頼できない入力がクエリのオブジェクト構造や演算子として解釈され、認証回避やデータ抽出、サーバー側JavaScriptの評価に至る欠陥です。SQLインジェクションと原理は同じでも、境界を壊す手段が文字列の連結からオブジェクトの注入へ移るため、文字のエスケープだけでは防げません。守りの起点は、値が本当に文字列なのかという型の確定と、$始まりの演算子キーを持ち込ませないことにあります。その上で、ドライバやODMの安全なクエリAPIでフィールドを固定し、$whereなどのサーバー側JavaScriptをやめ、最小権限で接続します。危険な文字を取り除く発想ではなく、命令とデータの型と構造を最初から分離する設計が出発点です。

NoSQLインジェクション対策チェックリスト

  • 文字列を期待する項目に、オブジェクトや配列が届いたら拒否する型検証を入口に置いているか
  • 検索条件へ渡すデータのキーに、$始まりやドットを含む名前があれば拒否しているか
  • 問い合わせを文字列で組み立てず、ドライバやODMのクエリAPIで各フィールドに検証済みの値を割り当てているか
  • MongooseのsanitizeFilterの有効化や、演算子オブジェクトの事前拒否で、$始まりキーの注入を無害化しているか
  • $whereやmapReduce、evalなどサーバー側JavaScriptを使わず、標準演算子や$exprに置き換えているか
  • データベース接続を最小権限にし、サーバー側スクリプティングの無効化とTLS、内部ネットワーク配置を確認したか

インジェクション全般の原理や、同じ根を持つ他の脆弱性への広げ方は、次の記事とあわせて読むと理解が定着します。

あわせて読みたい

OSコマンドインジェクションの仕組みと根本対策。シェルに渡さず引数を分離する

あわせて読みたい

OWASP Top 10とは。開発者が押さえるべきWebの代表的リスクと対策を一気に理解する

出典・参考

この記事をシェア

関連する記事

セキュアコーディング

入力バリデーションと出力エスケープの原則。入口で検証し、出口で文脈別にエスケープする

セキュアコーディングの土台である入力バリデーションと出力エスケープを、それぞれの役割の違いから整理します。入口の検証は防御の一段目、出口の文脈別エスケープがXSSやインジェクションの根本対策である理由を、原理と実務の判断基準まで掘り下げます。

セキュアコーディング

OSコマンドインジェクションの仕組みと根本対策。シェルに渡さず引数を分離する

OSコマンドインジェクション(CWE-78)が外部入力をシェルコマンドとして解釈させて成立する機構、OWASPのインジェクション分類での位置づけ、そしてシェルを介さず引数を配列で渡す根本対策までを開発者向けに整理します。入力検証が補助にとどまる理由も扱います。