こんにちは!セキュリティの世界へようこそ。
これからWeb開発やインフラの管理を始める新人エンジニアの皆さんにとって、「セキュリティ」ってなんだか難しそうで、壁が高く感じられますよね。でも、安心してください。一歩ずつ、身近な例から紐解いていけば、誰でもしっかりと安全なシステムを作れるようになります。
今日は、最近のモダンなWebサイトやアプリでよく使われている「MongoDB」などのNoSQL(エヌオーエスクーエル)というデータベースを狙った、ちょっと厄介な攻撃「NoSQLインジェクション」について、一緒に優しく学んでいきましょう!
—
1. 家の鍵に例えて考えてみよう!「NoSQLインジェクション」ってなに?
いきなり難しそうな名前が出てきましたが、まずは私たちの身近な「おうちの鍵」に例えて考えてみましょう。
想像してみてください。あなたは頑丈な玄関のドアを作りました。このドアには「合言葉(パスワード)」を知っている人だけが入れる仕組みになっています。
開発者の皆さんが作るログイン画面は、まさにこの「玄関のドア」です。
従来の古いデータベース(SQL)を使った攻撃は、泥棒が窓の隙間からピッキングツールを使って無理やり鍵を開けようとするようなイメージでした。
一方、今回のテーマであるNoSQLインジェクションは、泥棒がドアの鍵そのものを壊すのではなく、「鍵の仕組み(ルール)の勘違い」を巧みに利用して、まるで合言葉を言ったかのように錯覚させる手口なんです。
「えっ、そんなことができるの?」って思いますよね。
実は、私たちが良かれと思って使っている便利なシステムやプログラムの「融通の利きやすさ」が、そのまま攻撃者の武器になってしまうことがあるのです。
—
2. NoSQLと従来のデータベースはどう違うの?
そもそも、NoSQLって聞いたことがありますか?
従来のデータベース(リレーショナルデータベースと呼ばれます)は、エクセルの表のように「行と列」がきっちりと決まっていました。
これに対して、MongoDBなどに代表されるNoSQLは、もっと自由度の高い「段ボール箱に書類をポンと放り込んでいくような感覚」のデータベースです。データの形が決まっていないため、開発スピードが速く、現代のアプリ開発では大人気なんです。
しかし、この「何でも柔軟に受け入れてくれる優しさ」が、時として裏目に出てしまいます。
ログイン処理の裏側をのぞいてみよう
例えば、ユーザー名とパスワードを受け取って、データベースに問い合わせるJavaScriptのコード(Node.jsなど)を考えてみましょう。
// 【危険な実装例】ユーザーからの入力をそのままデータベースの検索に使っているケース
app.post('/login', async (req, res) => {
// 画面から送られてきたユーザー名とパスワードを受け取る
const username = req.body.username;
const password = req.body.password;
// データベースから、ユーザー名とパスワードが一致する人を探す
const user = await db.collection('users').findOne({
username: username,
password: password
});
if (user) {
res.send('ログイン成功!ようこそ!');
} else {
res.send('ユーザー名またはパスワードが違います。');
}
});
一見すると、何の問題もなさそうな綺麗なコードに見えますよね。
「画面に入力されたusernameとpasswordが、データベースの中身と一致するかどうかを調べる」という、ごく普通の処理です。
—
3. 攻撃者はどうやってこの仕組みを悪用するの?(演算子注入のからくり)
ここで、悪い人(攻撃者)の視点に立ってみましょう。
攻撃者は、パスワードの入力欄に、ただの文字列ではなく「データベースへの特別な命令(演算子)」をこっそり混ぜ込んで送信します。
NoSQLでは、条件を指定するために $ne(Not Equal=〜ではない)や $gt(Greater Than=〜より大きい)といった特別な記号(演算子)が用意されています。
もし攻撃者が、パスワードの入力欄に次のような特殊なデータ(JSONオブジェクトなど)を送り込んできたらどうなるでしょうか?
{
"$ne": ""
}
これはデータベースに対して、「空っぽの文字("")『ではない($ne)』パスワードを探しなさい」という命令になります。
世の中にあるほとんどのパスワードは空っぽではない(何か文字が入っている)ため、この条件は「常に正しい(True)」になってしまいます!
結果として、プログラムはパスワードの正当性を確認したつもりが、データベース側で「お、条件に合うからログインさせちゃおう!」と勘違いし、合言葉を知らないはずの攻撃者がすんなりログインできてしまうのです。これが演算子注入(オペレーターインジェクション)の正体です。
—
4. どうやって防ぐの? 現場で使える実践的な対策
「うわぁ、怖いですね……どうやってこの泥棒を防いだらいいんですか?」
安心してください。基本の対策をしっかり押さえておけば、こういった攻撃は完全にシャットアウトできます!
対策その1:入力値の「型」を厳格にチェックする(バリデーション)
一番大切なのは、「画面から送られてきたデータが、本当に期待している形(型)をしているか」を厳しくチェックすることです。
今回の例で言えば、パスワードやユーザー名は「ただの文字(文字列:String)」であるはずです。もし、入力されたデータの中に { や $ といった、データベースの命令に使われそうな怪しい記号やオブジェクト(構造体)が含まれていたら、その場で「おっと、それは受け取れません!」と弾き返せばいいのです。
実務のコードでは、次のように型をしっかりと検証しましょう。
// 【安全な実装例】入力された値が確実に「文字列」であることを確認する
const { body, validationResult } = require('express-validator');
app.post('/login', [
// ユーザー名とパスワードが「文字列」であることを強制する
body('username').isString().trim().escape(),
body('password').isString()
], async (req, res) => {
// バリデーションエラー(予期せぬ型や不正な文字)があれば処理をストップ
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ error: '入力内容に不正な形式が含まれています。' });
}
const username = req.body.username;
const password = req.body.password;
// 安全に検索処理を行う
const user = await db.collection('users').findOne({
username: username,
password: password
});
// 以下略...
});
対策その2:最新のライブラリやフレームワークの機能を正しく使う
私たちが自分で書くプログラムよりも、世界中の優秀なエンジニアたちがメンテナンスしている公式のライブラリやフレームワークの方が、セキュリティの落とし穴をうまく塞いでくれる機能を持っています。
例えば、MongooseなどのODM(Object Document Mapper)を使う場合も、クエリを構築する際にはユーザーからの入力をそのままオブジェクトとして渡さず、プリミティブな値(文字列や数値)に変換されていることを常に意識しましょう。
—
5. まとめ:一歩ずつ、安全な開発者を目指そう!
いかがでしたでしょうか?
NoSQLインジェクションは、一見すると難解な技術用語に思えますが、本質は「システムの『柔軟さ』や『思い込み』を逆手に取られた隙をつく攻撃」です。
- 「画面に入力されたものは、そのまま信じない」
- 「文字なのか、特別な命令(オブジェクト)なのかを必ずチェックする(型検証)」
この2つを心掛けるだけで、あなたの作るWebアプリケーションの安全性は劇的に向上します。
セキュリティ対策は、一朝一夕で完璧になるものではありません。日々の開発の中で、少しずつ「もし、ここに意地悪な入力をしてくる人がいたらどうなるだろう?」という視点(攻撃者の目線)を楽しみながら持てるようになると、ぐっとエンジニアとしての幅が広がりますよ。
一歩ずつ、頼もしいセキュリティ・マインドを育てていきましょう!応援しています!
コメント