【入門編】 NoSQLインジェクションの仕組みと防御 – オフェンシブセキュリティ & リバースエンジニアリング防御ガイド

こんにちは!Webアプリケーションの開発や、インフラの管理を任されるようになると、避けて通れないのが「セキュリティ」の壁ですよね。「なんだか難しそうな用語ばかりで、何から手をつければいいのか分からない…」と頭を悩ませていませんか?

今回は、新人のIT担当者や、セキュリティに初めて触れる開発者の皆さんに向けて、今とても勢いがあるデータベース「NoSQL(MongoDBなど)」のセキュリティの落とし穴である「NoSQLインジェクション」について、身近な防犯の仕組みに例えながら、優しく紐解いていきたいと思います。

一歩ずつ、確実に理解を深めていきましょう!

—

1. 家の鍵と泥棒に例える「NoSQLインジェクション」の正体

まずは、SQLやNoSQLといったデータベースの世界を、私たちの身近な「お家」に例えて考えてみましょう。

データベースというのは、いわば「大切な宝物をしまっておく金庫付きの大きなお家」です。そして、私たちが普段作るWebアプリ(ログイン画面など)は、そのお家の「玄関のインターホンと鍵」の役割をしています。

例えば、ログイン画面でユーザーが入力する「ID」と「パスワード」は、玄関の鍵を開けるための「合言葉」ですよね。
従来のSQLデータベースを狙った「SQLインジェクション」は、この合言葉の隙間を縫って「私は管理人です、全室の鍵を開けなさい!」と無理やり合言葉を書き換えて侵入する手口でした。

では、MongoDBなどのNoSQLを狙う攻撃はどうでしょうか?

NoSQLならではの「特殊な合言葉」の悪用

NoSQLデータベースは、従来の表形式(エクセルみたいな形式)ではなく、JSONのような柔軟な形式でデータを保存するのが特徴です。「今日はこういう条件のデータをください!」と、プログラム側で構築した条件(クエリ)をそのままデータベースに渡す仕組みになっています。

ここで、攻撃者は「文字」を入力する代わりに、データベース専用の「魔法の特殊命令(演算子)」をこっそり入力します。

例えば、MongoDBによくある $gt(「より大きい(Greater Than)」という意味の演算子)という特殊命令を考えてみましょう。
ログイン画面のパスワード入力欄に、人間が普通は入力しないような「パスワードなんて知らん!とにかくすべての中で、一番大きい値より上のものを全部出しなさい!」という命令を忍び込ませるのです。

お家に例えるなら、「合言葉を答える代わりに、ドアの鍵の仕組みそのものに『この条件に一致すれば何でも開く』という特殊な呪文を書き込んでしまう」ようなものです。
泥棒は鍵をピッキングするどころか、玄関のシステムをハッキングして「どんなパスワードでも無条件で通る」状態にして侵入してきてしまいます。これが、NoSQLインジェクションの恐ろしい仕組みです。

—

2. 脆弱なコードと攻撃のメカニズムを覗いてみよう

百聞は一見にしかず。実際のプログラムが、どのような油断から攻撃を許してしまうのかを見てみましょう。ここではNode.jsとMongoDBを例に挙げてみます。

以下のコードは、一見するとごく普通のログイン処理に見えますよね。

// 【危険な実装例】ユーザーからの入力をそのままクエリに使ってしまっているパターン
const express = require('express');
const router = express.Router();
const User = require('../models/user'); // ユーザーのデータモデル

router.post('/login', async (req, res) => {
    try {
        // ユーザーから送られてきたメールアドレスとパスワードをそのまま取得
        const userInputEmail = req.body.email;
        const userInputPassword = req.body.password;

        // データベースへ問い合わせるクエリを組み立てる
        // ※ここで入力をそのままオブジェクトに組み込んでいるのが最大の盲点!
        const user = await User.findOne({
            email: userInputEmail,
            password: userInputPassword
        });

        if (user) {
            // ログイン成功!
            res.json({ success: true, message: 'ログイン成功しました!' });
        } else {
            // ログイン失敗
            res.status(401).json({ success: false, message: 'メールアドレスかパスワードが違います。' });
        }
    } catch (err) {
        res.status(500).json({ error: 'サーバーエラーが発生しました。' });
    }
});

このコードのどこが危ないか分かりますか?
変数 userInputPassword に、もし悪意あるユーザーが文字ではなく、以下のようなオブジェクト(JSONデータ)を送り込んできたらどうなるでしょうか?

{
  "$gt": ""
}

データベースへの問い合わせ条件は、実質的に以下のようになってしまいます。

{
  email: "target@example.com",
  password: { $gt: "" } // 「空文字("")よりも大きいパスワードを持つユーザー」を探せ!
}

世の中のほとんどのパスワードは空文字より大きいので、結果としてパスワードを一切知らなくても、最初のユーザーとしてログインが成功してしまうのです。これがNoSQLインジェクションのカラクリです。

—

3. どうやって防ぐ? 泥棒をシャットアウトする2つの鉄則

それでは、このような巧妙な手口からアプリケーションを守るためにはどうすればよいのでしょうか? 対策は大きく分けて2つあります。防犯に例えてしっかりと押さえましょう。

対策その1:入力を厳格にチェックする(バリデーション)

お家の玄関に「怪しい荷物や、規格外の危険物が持ち込まれていないか」をチェックする屈強な警備員を配置するイメージです。

ユーザーから送られてきたデータが、本当に期待している「ただの文字列」や「ただの数値」なのかを、プログラムの入り口で厳しくチェック(バリデーション)します。先ほどの例であれば、パスワードやメールアドレスの入力値として、オブジェクト({})や特殊な演算子($から始まる文字列)が含まれていないかを弾くようにします。

対策その2:クエリ構築の抽象化と型安全性の確保

データベースへの手紙(クエリ)を、生データのまま組み立てるのをやめ、安全な専用の仕組み(ORM/ODMや安全なメソッド)を経由してやり取りします。

先ほどの危険なコードを、安全に書き直したサンプルを見てみましょう。

// 【安全な実装例】入力を文字列に強制変換し、型を担保するパターン
const express = require('express');
const router = express.Router();
const User = require('../models/user');

router.post('/login', async (req, res) => {
    try {
        const userInputEmail = req.body.email;
        const userInputPassword = req.body.password;

        // 【防御策A】入力値が「文字列(String)」であることを強制的に保証する
        // 万が一、攻撃者がオブジェクトや配列を送ってきたとしても、String()で強制的に文字に変換、
        // あるいはバリデーションライブラリで型を厳密にチェックして弾く。
        if (typeof userInputEmail !== 'string' || typeof userInputPassword !== 'string') {
            return res.status(400).json({ success: false, message: '不正な入力値です。' });
        }

        // データベースへの問い合わせ
        // 入力が確実に「ただの文字列」になっているため、$gtなどの特殊命令として解釈されない!
        const user = await User.findOne({
            email: userInputEmail,
            password: userInputPassword
        });

        if (user) {
            res.json({ success: true, message: 'ログイン成功しました!' });
        } else {
            res.status(401).json({ success: false, message: 'メールアドレスかパスワードが違います。' });
        }
    } catch (err) {
        res.status(500).json({ error: 'サーバーエラーが発生しました。' });
    }
});

このように、「ユーザーからの入力値は、たとえどんな形であれ信用してはならない(Never trust user input)」という原則を胸に刻み、必ず型をチェックする、あるいは文字列として明示的に扱うことが何よりも大切になります。

—

まとめ

いかがでしたでしょうか? NoSQLインジェクションは、NoSQLデータベース特有の柔軟性を逆手にとった巧妙な攻撃ですが、仕組みと対策を知ってしまえば怖くありません。

  • 攻撃の仕組み: 入力欄にデータの検索条件となる特殊な演算子($gtなど)を忍び込ませ、データベースの判定を狂わせる。
  • 防御の基本: ユーザーからの入力をそのままクエリに組み込まず、必ず型(文字列など)のバリデーションを行い、安全に扱う。

セキュリティの対策は、一度覚えてしまえば次の開発からは自然と手が動くようになります。ぜひ今日の学びをご自身のプロジェクトでも意識して、安全で堅牢なWebアプリケーションを作っていきましょう!

コメント

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