【実務・中級編】NoSQLインジェクション:クエリ演算子($gt, $ne等)による認証バイパス – アプリケーションセキュリティ & 安全な開発防御ガイド

SQLだけが敵じゃない:NoSQLインジェクションで「全開」になる認証の裏側

「うちはNoSQL(MongoDBとか)使ってるから、SQLインジェクションは関係ないよね」。
もし君がそう思っているなら、今日でその甘い認識は捨ててくれ。僕が過去に遭遇したインシデントでも、「セキュリティは万全」と言われていたシステムが、実はNoSQLインジェクションという極めて単純かつ致命的な穴から崩壊したケースが少なくない。

今日は、MongoDB等のNoSQL環境で「認証がスルーされる」という、エンジニアにとって最も悪夢に近い脆弱性のメカニズムと、それを黙らせるための実装ルールを叩き込む。

—

1. なぜ「認証」が突破されるのか?:NoSQLの盲点

SQLインジェクションが「文字列」を細工してクエリを書き換えるのに対し、NoSQLインジェクションは「型」や「構造」を悪用する。

例えば、MongoDBでよくあるログイン処理を見てみよう。ユーザーが入力した username と password を、そのままクエリに突っ込んでいるコードだ。

// 脆弱な実装例(Node.js/Express + MongoDB)
app.post(‘/login’, async (req, res) => {
const user = await db.collection(‘users’).findOne({
username: req.body.username,
password: req.body.password // ここが危ない!
});
if (user) { / ログイン成功 / }
});

攻撃者は、POSTリクエストのJSONを以下のように書き換える。

{
“username”: “admin”,
“password”: { “$gt”: “” }
}

何が起きているか?

MongoDBのクエリ演算子 $gt は「Greater Than(より大きい)」を意味する。
このリクエストが投げられると、内部で実行されるクエリはこうなる。
{ "username": "admin", "password": { "$gt": "" } }

「パスワードが空文字列より大きいユーザー」を探せという命令に変換されるんだ。当然、パスワードが設定されているユーザーはすべて条件に合致し、一番最初に見つかった admin アカウントで認証が突破されてしまう。これが「認証バイパス」の正体だ。

—

2. 実務で使える防御策:泥臭く「型」を縛る

「サニタイズすればいいんでしょ?」なんて生半可な知識で挑むな。サニタイズ漏れは必ず起きる。もっとも確実なのは、入力データの型を強制的に固定することだ。

Node.js(Express)での鉄板対策

リクエストが「オブジェクト」として送られてくることを許容してはいけない。パスワードは常に「文字列」であるべきだ。

// セキュアな実装例
const { body, validationResult } = require(‘express-validator’);

app.post(‘/login’, [
// 入力を強制的に文字列へ変換し、余計なオブジェクト注入を防ぐ
body(‘username’).isString().trim().escape(),
body(‘password’).isString().customSanitizer(val => String(val))
], async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(400).send(‘不正な入力です’);

const user = await db.collection(‘users’).findOne({
username: req.body.username,
password: req.body.password
});
// …以降ログイン処理
});

Python(PyMongo)の場合

Pythonでも同様だ。ライブラリ側で対策は進んでいるが、リクエストを受け取る際、Webフレームワーク(FastAPIやFlask)のPydanticモデルで型を厳密に定義しろ。

from pydantic import BaseModel, StrictStr

class LoginSchema(BaseModel):
# StrictStrを使うことで、辞書型(MongoDB演算子)の混入を拒否する
username: StrictStr
password: StrictStr

ルーター内でのバリデーション
def login(data: LoginSchema):
user = db.users.find_one({
“username”: data.username,
“password”: data.password
})

—

3. 運用レイヤーでの「最後の砦」

アプリケーション側の修正が完了するまでの間、あるいは多層防御の一環として、WAFでNoSQL演算子そのものを遮断するのも有効だ。

AWS WAF (Regex Pattern Set) の例:
リクエストボディの中に "$gt", "$ne", "$regex" といった文字列が含まれていないか監視するルールを設定する。

  • 正規表現: (\$gt|\$ne|\$regex|\$where|\$eq)
  • 対象: リクエストの全ペイロード

ただし、これは諸刃の剣だ。正規表現の書き方を誤ると、正常なユーザーの入力までブロックしてしまう。運用時には必ず「カウントモード(ログだけ取る)」で1週間ほど様子を見ろ。

—

現場のエンジニアへ送る「教訓」

僕がこれまで見てきた現場で、NoSQLインジェクションを食らうチームには共通点がある。それは「ライブラリの利便性に甘えて、入力をブラックボックスにしている」ことだ。

1. 入力は必ず「期待する型」にキャストする。
2. JSON全体を安易にデータベースのクエリに代入しない。
3. データベースのユーザー権限を最小化する(認証用DBユーザーには、読み取り以外の権限を一切与えないのが鉄則だ)。

セキュリティは「魔法のツール」を入れたら終わりじゃない。日々のコードの端々で、「このデータは本当に文字列か? 意図しない構造体が含まれていないか?」と疑う姿勢こそが、最強の防御になる。

さあ、今のプロジェクトのコードをもう一度見直してみてくれ。君のシステムを守れるのは、君自身しかいないんだから。

コメント

タイトルとURLをコピーしました