NoSQLインジェクションとSQLインジェクションの違い
NoSQLインジェクションは、SQLインジェクションと同様にデータベースへの不正なアクセスを可能にする攻撃ですが、その攻撃メカニズムと対策方法には大きな違いがあります。
攻撃メカニズムの根本的相違
NoSQLデータベースの多様性により、攻撃手法も多岐にわたります。SQLインジェクションがSQL文の構文を悪用するのに対し、NoSQLインジェクションは各データベース固有のクエリ言語や演算子を悪用します。
データ構造とクエリ言語の違い
| 項目 | SQL DB | NoSQL DB | セキュリティへの影響 |
|---|---|---|---|
| データ形式 | テーブル(行・列) | ドキュメント、KVS、グラフ、カラム | 攻撃対象の多様化 |
| クエリ言語 | SQL(標準化) | 独自API、JavaScript、JSON | 防御手法の複雑化 |
| スキーマ | 固定スキーマ | スキーマレス/柔軟 | 予期しないデータ構造への攻撃 |
| インジェクション手法 | SQL文の操作 | オブジェクト/演算子の操作 | 多様な攻撃パターン |
| 型システム | 厳格な型定義 | 動的型付け | 型の混乱を利用した攻撃 |
| エスケープ方法 | 標準的なエスケープ | DB固有の対策必要 | 統一的な対策の困難さ |
この違いにより、SQLインジェクション対策の知識だけではNoSQLインジェクションを防げません。各NoSQLデータベースの特性を理解し、それぞれに適した対策を実装する必要があります。
攻撃ベクトルの多様性
NoSQLデータベースでは、SQLとは異なる多様な攻撃ベクトルが存在します。
- JSONインジェクション
- JSONオブジェクトの構造を悪用した攻撃で、特にMongoDBやCouchDBで頻繁に見られます。演算子($ne、$gt、$regex等)を注入することで、意図しない条件でのデータ取得や認証バイパスを引き起こします。例えば、`{"password": {"$ne": ""}}`というペイロードで、パスワードが空でない全てのユーザーとしてログインできる可能性があります。
- JavaScriptインジェクション
- MongoDBの$where句やMapReduce機能、RedisのLuaスクリプトなど、サーバーサイドでスクリプトを実行する機能を悪用します。任意のコード実行により、データの窃取、改ざん、削除だけでなく、サーバー自体の制御を奪われる可能性もあります。特に古いバージョンのMongoDBでは、デフォルトでJavaScript実行が有効になっているため注意が必要です。
- 演算子インジェクション
- NoSQLデータベース特有の演算子($ne、$gt、$lt、$regex等)を悪用する攻撃です。これらの演算子を使用することで、本来アクセスできないデータへのアクセスや、条件を満たさないはずの認証を通過させることが可能になります。特に認証処理での演算子インジェクションは、システム全体のセキュリティを脅かす重大な脆弱性となります。
- プロトタイプ汚染攻撃
- JavaScriptベースのNoSQLシステムで、オブジェクトのプロトタイプチェーンを汚染する攻撃です。`__proto__`や`constructor`プロパティを悪用し、アプリケーション全体に影響を与える可能性があります。
影響範囲と被害パターン
NoSQLインジェクションによる被害は、SQLインジェクションと同等またはそれ以上に深刻になる場合があります。
NoSQL特有のリスク
認証バイパス(演算子悪用)は、最も一般的で危険な攻撃パターンです。MongoDBの$ne(not equal)演算子を使用することで、パスワードを知らなくても認証を通過できる場合があります。
データ漏洩(全件取得)では、適切な条件指定なしに全データを取得される可能性があります。NoSQLデータベースは大量データの処理に優れているため、一度の攻撃で膨大なデータが流出するリスクがあります。
DoS攻撃(正規表現、無限ループ)により、複雑な正規表現や無限ループするJavaScriptコードを実行させることで、データベースサーバーのリソースを枯渇させ、サービス停止に追い込むことができます。
Remote Code Execution(JavaScript実行)は、最も深刻な被害をもたらす可能性があります。サーバー上で任意のコードを実行できれば、データベースだけでなくシステム全体が危険にさらされます。
MongoDBのインジェクション対策
MongoDBは最も人気のあるNoSQLデータベースの一つですが、その柔軟性ゆえに多くのセキュリティリスクを抱えています。
典型的な攻撃パターン
MongoDBへの攻撃は、主に演算子の悪用とJavaScript実行を利用して行われます。
演算子インジェクションの脅威
// 脆弱なコード例:認証処理
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// 危険:ユーザー入力をそのままクエリに使用
const user = await db.collection('users').findOne({
username: username,
password: password
});
if (user) {
req.session.userId = user._id;
res.json({ success: true, message: 'ログイン成功' });
} else {
res.status(401).json({ success: false, message: '認証失敗' });
}
});
// 攻撃例1:演算子を使った認証バイパス
// 以下のJSONをPOSTリクエストのボディとして送信
{
"username": "admin",
"password": {"$ne": ""} // パスワードが空文字でなければ何でもOK
}
// 攻撃例2:正規表現を使った総当たり攻撃
{
"username": "admin",
"password": {"$regex": "^a.*"} // 'a'で始まるパスワードを探索
}
// 攻撃例3:$gtを使った条件操作
{
"username": {"$gt": ""}, // 空文字より大きい(=全ユーザー)
"password": {"$gt": ""}
}
// さらに危険な例:$where句によるJavaScript実行
app.get('/search', async (req, res) => {
const query = req.query.q;
// 極めて危険:ユーザー入力をJavaScriptとして実行
const results = await db.collection('products').find({
$where: `this.name.includes('${query}')`
}).toArray();
res.json(results);
});
// 攻撃ペイロード
// ?q='); while(true){} //
// ?q='); return true; var x='
セキュアな実装方法
MongoDBを安全に使用するためには、多層的な防御策を実装する必要があります。
入力検証とサニタイゼーション
const mongoose = require('mongoose');
const validator = require('validator');
const bcrypt = require('bcrypt');
// 安全な実装例:完全な入力検証を含む認証処理
class SecureAuthController {
constructor() {
// MongoDBのスキーマ定義(Mongoose使用)
this.userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
trim: true,
minlength: 3,
maxlength: 30,
validate: {
validator: (v) => /^[a-zA-Z0-9_]+$/.test(v),
message: 'ユーザー名は英数字とアンダースコアのみ使用可能'
}
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
validate: [validator.isEmail, 'メールアドレスが無効です']
},
passwordHash: {
type: String,
required: true
},
loginAttempts: {
type: Number,
default: 0
},
lockUntil: Date,
createdAt: {
type: Date,
default: Date.now
}
});
this.User = mongoose.model('User', this.userSchema);
}
// 入力のサニタイゼーション関数
sanitizeInput(input) {
// オブジェクトや配列の場合は拒否
if (typeof input !== 'string') {
throw new Error('Invalid input type');
}
// 危険な文字をエスケープ
return validator.escape(input)
.replace(/\$/g, '') // $記号を除去
.replace(/\./g, '') // ドット記法を防ぐ
.replace(/\[/g, '') // 配列記法を防ぐ
.replace(/\]/g, '');
}
// 型チェックを含む安全なログイン処理
async login(req, res) {
try {
// 1. 入力の型チェック
if (!req.body || typeof req.body !== 'object') {
return res.status(400).json({
success: false,
message: '無効なリクエスト'
});
}
const { username, password } = req.body;
// 2. 基本的な検証
if (typeof username !== 'string' || typeof password !== 'string') {
return res.status(400).json({
success: false,
message: '入力形式が不正です'
});
}
// 3. 長さチェック
if (username.length < 3 || username.length > 30 ||
password.length < 8 || password.length > 100) {
return res.status(400).json({
success: false,
message: '入力値の長さが不適切です'
});
}
// 4. サニタイゼーション(ユーザー名のみ)
const sanitizedUsername = this.sanitizeInput(username);
// 5. レート制限チェック(ブルートフォース対策)
const user = await this.User.findOne({
username: sanitizedUsername
}).select('+passwordHash +loginAttempts +lockUntil');
if (user && user.lockUntil && user.lockUntil > Date.now()) {
return res.status(429).json({
success: false,
message: 'アカウントがロックされています',
lockUntil: user.lockUntil
});
}
// 6. パスワード検証(bcryptでハッシュ比較)
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
// ログイン失敗時の処理
if (user) {
user.loginAttempts += 1;
if (user.loginAttempts >= 5) {
user.lockUntil = Date.now() + 30 * 60 * 1000; // 30分ロック
}
await user.save();
}
return res.status(401).json({
success: false,
message: '認証に失敗しました'
});
}
// 7. ログイン成功時の処理
user.loginAttempts = 0;
user.lockUntil = undefined;
await user.save();
// セッション生成(JWTなど)
const token = this.generateToken(user);
res.json({
success: true,
message: 'ログイン成功',
token: token
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({
success: false,
message: 'サーバーエラーが発生しました'
});
}
}
// 安全な検索処理の実装
async safeSearch(req, res) {
try {
const searchTerm = req.query.q;
// 型チェック
if (typeof searchTerm !== 'string') {
return res.status(400).json({
success: false,
message: '検索キーワードが不正です'
});
}
// サニタイゼーション
const sanitized = validator.escape(searchTerm)
.replace(/[^\w\s日本語]/gi, ''); // 特殊文字を除去
// 安全なクエリ構築(正規表現を使わない)
const results = await this.User.find({
$or: [
{ username: { $regex: new RegExp(sanitized, 'i') } },
{ email: { $regex: new RegExp(sanitized, 'i') } }
]
})
.select('-passwordHash') // パスワードハッシュは除外
.limit(20) // 結果数を制限
.exec();
res.json({
success: true,
results: results
});
} catch (error) {
console.error('Search error:', error);
res.status(500).json({
success: false,
message: '検索中にエラーが発生しました'
});
}
}
}
MongoDBセキュリティ設定
アプリケーション層の対策に加え、MongoDB自体のセキュリティ設定も重要です。
設定ファイルの最適化
# mongod.conf - セキュアな設定例
# MongoDB 5.0以降推奨設定
# ネットワーク設定
net:
port: 27017
bindIp: 127.0.0.1 # ローカルホストのみに制限
# 本番環境では特定のIPのみ許可
# bindIp: 127.0.0.1,10.0.0.5,10.0.0.6
maxIncomingConnections: 100
ssl:
mode: requireSSL # TLS/SSL必須
PEMKeyFile: /etc/mongodb/mongodb.pem
CAFile: /etc/mongodb/ca.pem
allowConnectionsWithoutCertificates: false
# セキュリティ設定
security:
authorization: enabled # 認証を有効化
javascriptEnabled: false # JavaScript実行を無効化(重要)
# Kerberosやx.509認証も設定可能
sasl:
hostName: mongodb.example.com
serviceName: mongodb
# 監査設定(Enterprise版)
auditLog:
destination: file
format: JSON
path: /var/log/mongodb/auditLog.json
filter: '{ atype: { $in: [ "authenticate", "createCollection", "dropCollection" ] } }'
# パフォーマンスとセキュリティのバランス
setParameter:
failIndexKeyTooLong: false
notablescan: false # 本番では有効化を検討
authenticationMechanisms: SCRAM-SHA-256 # 強力な認証メカニズム
# JavaScriptを完全に無効化
javascriptProtection: true
# ログ設定
systemLog:
destination: file
path: /var/log/mongodb/mongod.log
logAppend: true
logRotate: reopen
verbosity: 1
component:
accessControl:
verbosity: 2 # アクセス制御のログレベルを上げる
# ストレージ設定
storage:
dbPath: /var/lib/mongodb
journal:
enabled: true
engine: wiredTiger
wiredTiger:
engineConfig:
cacheSizeGB: 2
collectionConfig:
blockCompressor: snappy
# 保存時暗号化(Enterprise版)
encryptionKeyFile: /etc/mongodb/encryption-key
# プロセス管理
processManagement:
fork: true
pidFilePath: /var/run/mongodb/mongod.pid
# オペレーションプロファイリング
operationProfiling:
mode: slowOp
slowOpThresholdMs: 100
slowOpSampleRate: 1.0
ロールベースアクセス制御
// MongoDB シェルでのユーザーとロール管理
// 管理者ユーザーの作成
db.createUser({
user: "adminUser",
pwd: passwordPrompt(), // パスワードは対話的に入力
roles: [
{ role: "userAdminAnyDatabase", db: "admin" },
{ role: "dbAdminAnyDatabase", db: "admin" },
{ role: "readWriteAnyDatabase", db: "admin" }
],
mechanisms: ["SCRAM-SHA-256"],
passwordDigestor: "server"
});
// アプリケーション用ユーザー(最小権限)
db.createUser({
user: "appUser",
pwd: passwordPrompt(),
roles: [
{
role: "readWrite",
db: "myapp"
}
],
mechanisms: ["SCRAM-SHA-256"],
authenticationRestrictions: [
{
clientSource: ["10.0.0.0/24"], // 特定のIPレンジのみ
serverAddress: ["10.0.0.100"]
}
]
});
// カスタムロールの作成(細かい権限制御)
db.createRole({
role: "appReadOnly",
privileges: [
{
resource: { db: "myapp", collection: "users" },
actions: ["find", "count"]
},
{
resource: { db: "myapp", collection: "products" },
actions: ["find"]
}
],
roles: []
});
// レポート用ユーザー(読み取り専用)
db.createUser({
user: "reportUser",
pwd: passwordPrompt(),
roles: [
{ role: "appReadOnly", db: "myapp" }
],
mechanisms: ["SCRAM-SHA-256"]
});
// バックアップ用ユーザー
db.createUser({
user: "backupUser",
pwd: passwordPrompt(),
roles: [
{ role: "backup", db: "admin" },
{ role: "restore", db: "admin" }
]
});
// ユーザー権限の確認
db.runCommand({
usersInfo: "appUser",
showPrivileges: true,
showAuthenticationRestrictions: true
});
Redisのコマンドインジェクション防御
Redisは高速なインメモリデータストアとして広く利用されていますが、適切な設定なしには深刻なセキュリティリスクを抱えます。
Redisの脆弱性パターン
Redisの脆弱性は、危険なコマンドの実行とLuaスクリプトの悪用が主な攻撃経路となります。
危険なコマンドと攻撃手法
| コマンド | リスクレベル | 攻撃例 | 影響 |
|---|---|---|---|
| EVAL/EVALSHA | 致命的 | Luaコード実行 | 任意コード実行、無限ループ |
| CONFIG SET | 致命的 | 設定変更 | 認証無効化、ファイル書き込み |
| FLUSHALL/FLUSHDB | 高 | 全データ削除 | DoS攻撃、データ損失 |
| SCRIPT LOAD | 高 | スクリプト登録 | バックドア設置 |
| KEYS * | 中 | 全キー列挙 | 情報漏洩、性能劣化 |
| SAVE/BGSAVE | 中 | ディスク書き込み | ディスクDoS |
| DEBUG | 高 | デバッグ機能悪用 | メモリダンプ、クラッシュ |
| MODULE LOAD | 致命的 | モジュール読み込み | 任意コード実行 |
Redisセキュリティ強化策
Redis 6.0以降で導入されたACL(Access Control List)を活用した、きめ細かいアクセス制御を実装します。
ACLによるコマンド制限
# Redis ACL設定例(redis.conf または実行時設定)
# デフォルトユーザーを無効化
ACL SETUSER default off
# 管理者ユーザー(全権限)
ACL SETUSER admin on >AdminPassword123! ~* &* +@all
# アプリケーションユーザー(制限付き)
ACL SETUSER appuser on >AppPassword456! ~* &* \
+@read +@write +@list +@set +@sortedset +@hash +@stream \
-FLUSHDB -FLUSHALL -KEYS -CONFIG -EVAL -SCRIPT \
-DEBUG -MODULE -ACL -SAVE -BGSAVE -SHUTDOWN
# キャッシュ専用ユーザー(特定プレフィックスのみ)
ACL SETUSER cacheuser on >CachePass789! ~cache:* ~session:* \
+GET +SET +DEL +EXISTS +EXPIRE +TTL \
-FLUSHDB -FLUSHALL
# 読み取り専用ユーザー
ACL SETUSER readonly on >ReadOnlyPass! ~* &* +@read \
-KEYS -SCAN -SORT
# 統計情報取得用ユーザー
ACL SETUSER monitoring on >MonitorPass! ~* &* \
+PING +INFO +CLIENT +CONFIG|GET +DBSIZE +LASTSAVE
# セッション管理用ユーザー(厳格な制限)
ACL SETUSER sessionmgr on >SessionPass! \
~sess:* \
+GET +SET +DEL +EXPIRE +TTL +EXISTS \
-KEYS -SCAN
# ACL設定の保存
ACL SAVE
# 現在のACL設定を確認
ACL LIST
# ユーザーの権限を確認
ACL GETUSER appuser
Redisクライアントでの対策
import redis
import re
import hashlib
from typing import Any, Optional, List
import json
class SecureRedisClient:
"""セキュアなRedisクライアント実装"""
def __init__(self, host='localhost', port=6379, password=None,
db=0, ssl=True, ssl_cert_reqs='required'):
"""
安全な設定でRedisクライアントを初期化
"""
self.connection_pool = redis.ConnectionPool(
host=host,
port=port,
password=password,
db=db,
socket_connect_timeout=5,
socket_timeout=5,
socket_keepalive=True,
socket_keepalive_options={},
connection_class=redis.SSLConnection if ssl else redis.Connection,
ssl_cert_reqs=ssl_cert_reqs if ssl else None,
ssl_ca_certs='/etc/redis/ca.crt' if ssl else None,
ssl_certfile='/etc/redis/redis.crt' if ssl else None,
ssl_keyfile='/etc/redis/redis.key' if ssl else None,
max_connections=50,
decode_responses=True
)
self.client = redis.Redis(connection_pool=self.connection_pool)
# 危険なコマンドのブラックリスト
self.blocked_commands = {
'EVAL', 'EVALSHA', 'SCRIPT', 'CONFIG',
'FLUSHDB', 'FLUSHALL', 'SHUTDOWN', 'DEBUG',
'MODULE', 'ACL', 'BGSAVE', 'SAVE',
'SLAVEOF', 'REPLICAOF', 'MIGRATE'
}
# 許可されたコマンドのホワイトリスト(オプション)
self.allowed_commands = {
'GET', 'SET', 'DEL', 'EXISTS', 'EXPIRE', 'TTL',
'INCR', 'DECR', 'LPUSH', 'RPUSH', 'LPOP', 'RPOP',
'SADD', 'SREM', 'SMEMBERS', 'HGET', 'HSET', 'HDEL',
'ZADD', 'ZREM', 'ZRANGE', 'ZREVRANGE'
}
def sanitize_key(self, key: str) -> str:
"""
キー名をサニタイズ
"""
if not isinstance(key, str):
raise ValueError("Key must be a string")
# 長さチェック
if len(key) == 0 or len(key) > 512:
raise ValueError("Key length must be between 1 and 512")
# 危険な文字をフィルタリング
# 英数字、コロン、アンダースコア、ハイフンのみ許可
sanitized = re.sub(r'[^a-zA-Z0-9:_\-]', '', key)
# Luaコードインジェクションを防ぐ
if any(pattern in sanitized.lower() for pattern in
['eval', 'script', 'load', 'kill', 'flush']):
raise ValueError("Potentially dangerous key pattern detected")
return sanitized
def sanitize_value(self, value: Any) -> str:
"""
値をサニタイズ
"""
if value is None:
return ''
# 基本型の処理
if isinstance(value, (int, float)):
return str(value)
if isinstance(value, str):
# 制御文字を除去
sanitized = re.sub(r'[\x00-\x1f\x7f-\x9f]', '', value)
# 最大長制限
if len(sanitized) > 1048576: # 1MB
raise ValueError("Value too large")
return sanitized
# オブジェクトはJSON化
if isinstance(value, (dict, list)):
json_str = json.dumps(value, ensure_ascii=True)
if len(json_str) > 1048576:
raise ValueError("Serialized value too large")
return json_str
return str(value)
def safe_get(self, key: str) -> Optional[str]:
"""
安全なGET操作
"""
safe_key = self.sanitize_key(key)
return self.client.get(safe_key)
def safe_set(self, key: str, value: Any, ex: int = None,
nx: bool = False, xx: bool = False) -> bool:
"""
安全なSET操作
"""
safe_key = self.sanitize_key(key)
safe_value = self.sanitize_value(value)
# 有効期限の検証
if ex is not None:
if not isinstance(ex, int) or ex < 1 or ex > 31536000: # 最大1年
raise ValueError("Invalid expiration time")
return self.client.set(safe_key, safe_value, ex=ex, nx=nx, xx=xx)
def safe_delete(self, *keys: str) -> int:
"""
安全なDEL操作
"""
safe_keys = [self.sanitize_key(key) for key in keys]
return self.client.delete(*safe_keys)
def safe_execute_command(self, command: str, *args) -> Any:
"""
コマンドの安全な実行
"""
# コマンドの検証
cmd_upper = command.upper()
# ブロックリストチェック
if cmd_upper in self.blocked_commands:
raise ValueError(f"Command {command} is blocked for security reasons")
# ホワイトリストモード(有効な場合)
# if cmd_upper not in self.allowed_commands:
# raise ValueError(f"Command {command} is not allowed")
# 引数のサニタイゼーション
safe_args = []
for arg in args:
if isinstance(arg, str):
# コマンドインジェクション防止
if re.search(r'[;&|`$]', arg):
raise ValueError("Potentially dangerous characters in argument")
safe_args.append(self.sanitize_value(arg))
else:
safe_args.append(arg)
# 実行
return self.client.execute_command(command, *safe_args)
def create_session(self, user_id: str, session_data: dict,
ttl: int = 3600) -> str:
"""
セキュアなセッション作成
"""
# セッションIDの生成(暗号学的に安全)
session_id = hashlib.sha256(
f"{user_id}:{json.dumps(session_data)}:{redis.time()}".encode()
).hexdigest()
session_key = f"session:{session_id}"
# セッションデータの保存
self.safe_set(session_key, session_data, ex=ttl)
return session_id
def rate_limit_check(self, identifier: str, limit: int = 100,
window: int = 60) -> bool:
"""
レート制限チェック(スライディングウィンドウ)
"""
key = f"rate_limit:{self.sanitize_key(identifier)}"
try:
pipe = self.client.pipeline()
now = redis.time()
# 古いエントリを削除
pipe.zremrangebyscore(key, 0, now - window)
# 現在のカウントを取得
pipe.zcard(key)
# 新しいエントリを追加
pipe.zadd(key, {f"{now}:{redis.uuid()}": now})
# TTLを設定
pipe.expire(key, window + 1)
results = pipe.execute()
current_count = results[1]
return current_count < limit
except redis.RedisError as e:
# エラー時は安全側に倒す(制限する)
print(f"Rate limit check error: {e}")
return False
# 使用例
def main():
# セキュアなRedisクライアントの初期化
redis_client = SecureRedisClient(
host='localhost',
port=6379,
password='SecurePassword123!',
ssl=True
)
# 安全な操作の実行
try:
# ユーザーデータの保存
user_data = {
'username': 'john_doe',
'email': 'john@example.com',
'role': 'user'
}
redis_client.safe_set('user:123', user_data, ex=3600)
# データの取得
data = redis_client.safe_get('user:123')
if data:
user = json.loads(data)
print(f"User: {user}")
# レート制限チェック
ip_address = '192.168.1.100'
if redis_client.rate_limit_check(ip_address, limit=10, window=60):
print("Request allowed")
else:
print("Rate limit exceeded")
except ValueError as e:
print(f"Validation error: {e}")
except redis.RedisError as e:
print(f"Redis error: {e}")
if __name__ == "__main__":
main()
ElasticsearchとCouchDBの脆弱性
ElasticsearchとCouchDBは、それぞれ異なる用途で使用されるNoSQLデータベースですが、独自のセキュリティ課題を抱えています。
Elasticsearchのインジェクション対策
Elasticsearchは全文検索エンジンとして優れていますが、スクリプト実行機能が脆弱性の原因となることがあります。
Script Injectionの防御
// Elasticsearchセキュリティ設定(elasticsearch.yml)
# スクリプト実行の制限
script.allowed_types: inline
script.allowed_contexts: none
# 危険なAPIの無効化
action.destructive_requires_name: true
rest.action.multi.allow_explicit_index: false
# ネットワーク設定
network.host: 10.0.0.10
http.port: 9200
transport.port: 9300
# セキュリティ機能の有効化(X-Pack)
xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true
xpack.security.transport.ssl.verification_mode: certificate
xpack.security.http.ssl.enabled: true
# 監査ログ
xpack.security.audit.enabled: true
xpack.security.audit.outputs: [index, logfile]
// Node.jsでの安全なElasticsearchクエリ実装
const { Client } = require('@elastic/elasticsearch');
class SecureElasticsearchClient {
constructor() {
this.client = new Client({
node: 'https://localhost:9200',
auth: {
username: 'elastic',
password: process.env.ELASTIC_PASSWORD
},
ssl: {
ca: fs.readFileSync('./ca.crt'),
rejectUnauthorized: true
}
});
}
// 安全な検索実装
async safeSearch(index, searchTerm, filters = {}) {
// 入力検証
if (typeof searchTerm !== 'string') {
throw new Error('Search term must be a string');
}
// 特殊文字のエスケープ
const escapedTerm = this.escapeElasticQuery(searchTerm);
// テンプレート化されたクエリの使用
const searchParams = {
index: index,
body: {
query: {
bool: {
must: [
{
match: {
content: {
query: escapedTerm,
operator: 'and',
fuzziness: 'AUTO'
}
}
}
],
filter: this.buildSecureFilters(filters)
}
},
// スクリプトは使用しない
size: 20,
from: 0,
_source: {
excludes: ['password', 'secret', 'token']
},
timeout: '10s'
}
};
try {
const result = await this.client.search(searchParams);
return this.sanitizeSearchResults(result.body.hits);
} catch (error) {
console.error('Search error:', error);
throw new Error('Search failed');
}
}
// Elasticsearchクエリ文字列のエスケープ
escapeElasticQuery(str) {
// 特殊文字をエスケープ
return str.replace(/[+\-=&|!(){}[\]^"~*?:\\/]/g, '\\$&');
}
// セキュアなフィルター構築
buildSecureFilters(filters) {
const secureFilters = [];
for (const [field, value] of Object.entries(filters)) {
// フィールド名の検証
if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(field)) {
continue; // 不正なフィールド名は無視
}
// 値の型に応じた処理
if (typeof value === 'string') {
secureFilters.push({
term: { [field]: value }
});
} else if (typeof value === 'number') {
secureFilters.push({
term: { [field]: value }
});
} else if (Array.isArray(value)) {
secureFilters.push({
terms: { [field]: value.filter(v => typeof v === 'string' || typeof v === 'number') }
});
}
}
return secureFilters;
}
// 結果のサニタイズ
sanitizeSearchResults(hits) {
return hits.hits.map(hit => ({
id: hit._id,
score: hit._score,
source: this.removeSecretFields(hit._source)
}));
}
// 機密フィールドの除去
removeSecretFields(doc) {
const secretFields = ['password', 'passwordHash', 'apiKey', 'token', 'secret'];
const cleaned = { ...doc };
secretFields.forEach(field => {
delete cleaned[field];
});
return cleaned;
}
// インデックスの作成(セキュアな設定)
async createSecureIndex(indexName) {
await this.client.indices.create({
index: indexName,
body: {
settings: {
'index.blocks.read_only_allow_delete': false,
'index.max_result_window': 10000,
'index.max_inner_result_window': 100,
'index.max_script_fields': 0, // スクリプトフィールド無効
'index.requests.cache.enable': true
},
mappings: {
properties: {
content: { type: 'text', analyzer: 'standard' },
created_at: { type: 'date' },
user_id: { type: 'keyword' },
// 動的マッピングを制限
dynamic: 'strict'
}
}
}
});
}
}
CouchDBのビュー関数セキュリティ
CouchDBのMapReduce機能は強力ですが、不適切な実装は脆弱性につながります。
MapReduce関数の安全な実装
// CouchDBセキュア設定とビュー実装
// 1. セキュアなデザインドキュメント
const secureDesignDoc = {
_id: '_design/secure_views',
language: 'javascript',
// バリデーション関数(重要)
validate_doc_update: function(newDoc, oldDoc, userCtx, secObj) {
// 型チェック
if (newDoc.type && typeof newDoc.type !== 'string') {
throw {forbidden: 'Document type must be a string'};
}
// ユーザー権限チェック
if (newDoc.type === 'admin' && userCtx.roles.indexOf('admin') === -1) {
throw {forbidden: 'Only admins can create admin documents'};
}
// 入力検証(例:メールアドレス)
if (newDoc.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newDoc.email)) {
throw {forbidden: 'Invalid email format'};
}
// SQLインジェクション的な攻撃パターンの検出
const dangerousPatterns = [
/\$where/i,
/function\s*\(/,
/eval\s*\(/,
/\bexec\b/i
];
const docString = JSON.stringify(newDoc);
for (let pattern of dangerousPatterns) {
if (pattern.test(docString)) {
throw {forbidden: 'Potentially dangerous content detected'};
}
}
// サイズ制限
if (docString.length > 1048576) { // 1MB
throw {forbidden: 'Document too large'};
}
}.toString(),
// セキュアなビュー定義
views: {
// ユーザー一覧(安全な実装)
by_username: {
map: function(doc) {
// 厳格な型チェック
if (doc.type === 'user' && doc.username && typeof doc.username === 'string') {
// 機密情報は含めない
emit(doc.username, {
id: doc._id,
email: doc.email,
created: doc.created_at
// passwordやtokenは含めない
});
}
}.toString()
},
// 日付範囲検索(インジェクション対策済み)
by_date: {
map: function(doc) {
if (doc.type === 'event' && doc.date) {
// ISO日付形式の検証
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(doc.date)) {
emit(doc.date, {
title: doc.title,
category: doc.category
});
}
}
}.toString()
},
// 集計用ビュー(reduce関数を含む)
count_by_type: {
map: function(doc) {
if (doc.type && typeof doc.type === 'string') {
emit(doc.type, 1);
}
}.toString(),
reduce: function(keys, values, rereduce) {
// 安全な集計処理
return sum(values);
}.toString()
}
},
// リスト関数(出力のフォーマット)
lists: {
safe_json: function(head, req) {
// ヘッダー設定
provides('json', function() {
send('{"rows":[');
var row;
var first = true;
while (row = getRow()) {
// XSS対策:HTMLエスケープ
var safeValue = JSON.stringify(row.value)
.replace(/</g, '\\u003c')
.replace(/>/g, '\\u003e')
.replace(/&/g, '\\u0026');
if (!first) send(',');
send(safeValue);
first = false;
}
send(']}');
});
}.toString()
}
};
// 2. Node.jsでの安全なCouchDBクライアント実装
const nano = require('nano');
class SecureCouchDBClient {
constructor(url, dbName) {
this.nano = nano({
url: url,
requestDefaults: {
timeout: 10000,
headers: {
'X-CouchDB-WWW-Authenticate': 'Cookie',
'Content-Type': 'application/json'
}
}
});
this.db = this.nano.db.use(dbName);
}
// 安全なドキュメント作成
async createDocument(doc) {
// 入力検証
this.validateDocument(doc);
// _idの生成(UUIDv4)
if (!doc._id) {
doc._id = this.generateSecureId();
}
// タイムスタンプ追加
doc.created_at = new Date().toISOString();
doc.updated_at = doc.created_at;
try {
const response = await this.db.insert(doc);
return response;
} catch (error) {
console.error('Document creation error:', error);
throw new Error('Failed to create document');
}
}
// ドキュメント検証
validateDocument(doc) {
// オブジェクトチェック
if (typeof doc !== 'object' || doc === null) {
throw new Error('Document must be an object');
}
// 危険なキーの検出
const dangerousKeys = ['$where', 'mapReduce', '_design'];
for (let key of dangerousKeys) {
if (key in doc) {
throw new Error(`Dangerous key detected: ${key}`);
}
}
// 再帰的な検証
this.deepValidate(doc);
}
// 深い検証
deepValidate(obj, depth = 0) {
if (depth > 10) {
throw new Error('Document too deeply nested');
}
for (let [key, value] of Object.entries(obj)) {
// キー名の検証
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key) && key !== '_id' && key !== '_rev') {
throw new Error(`Invalid key name: ${key}`);
}
// 値の検証
if (typeof value === 'object' && value !== null) {
this.deepValidate(value, depth + 1);
} else if (typeof value === 'string') {
// 文字列長制限
if (value.length > 65536) {
throw new Error('String value too long');
}
}
}
}
// セキュアなビュークエリ
async queryView(designDoc, viewName, options = {}) {
// オプションの検証
const safeOptions = this.sanitizeViewOptions(options);
try {
const response = await this.db.view(designDoc, viewName, safeOptions);
return this.sanitizeViewResults(response);
} catch (error) {
console.error('View query error:', error);
throw new Error('View query failed');
}
}
// ビューオプションのサニタイズ
sanitizeViewOptions(options) {
const safe = {};
// 許可されたオプションのみ
const allowed = ['key', 'keys', 'startkey', 'endkey', 'limit', 'skip', 'descending', 'include_docs'];
for (let opt of allowed) {
if (opt in options) {
if (opt === 'limit' || opt === 'skip') {
// 数値検証
const num = parseInt(options[opt]);
if (!isNaN(num) && num >= 0 && num <= 1000) {
safe[opt] = num;
}
} else {
safe[opt] = options[opt];
}
}
}
return safe;
}
// 結果のサニタイズ
sanitizeViewResults(response) {
if (!response.rows || !Array.isArray(response.rows)) {
return { rows: [] };
}
return {
total_rows: response.total_rows,
offset: response.offset,
rows: response.rows.map(row => ({
id: row.id,
key: row.key,
value: this.removeSecretFields(row.value)
}))
};
}
// 機密フィールドの除去
removeSecretFields(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
const cleaned = { ...obj };
const secretFields = ['password', 'token', 'apiKey', 'secret'];
secretFields.forEach(field => {
delete cleaned[field];
});
return cleaned;
}
// セキュアなID生成
generateSecureId() {
const crypto = require('crypto');
return crypto.randomBytes(16).toString('hex');
}
}
// 使用例
async function main() {
const client = new SecureCouchDBClient(
'http://admin:password@localhost:5984',
'mydb'
);
// デザインドキュメントの作成
await client.db.insert(secureDesignDoc);
// 安全なドキュメント作成
const newDoc = await client.createDocument({
type: 'user',
username: 'john_doe',
email: 'john@example.com'
});
// 安全なビュークエリ
const results = await client.queryView('secure_views', 'by_username', {
key: 'john_doe',
include_docs: false
});
console.log('Results:', results);
}
JSONインジェクションの脅威と対策
JSONインジェクションは、NoSQLデータベース特有の脅威として、深刻な脆弱性を引き起こす可能性があります。
JSONパーサーの脆弱性
JSONパーサーの実装上の問題や、不適切な使用により様々な攻撃が可能になります。
NoSQL環境でのセキュリティベストプラクティス
NoSQLデータベースを安全に運用するためには、多層的な防御アプローチが不可欠です。
多層防御アプローチ
セキュリティは単一の対策では不十分であり、複数の防御層を組み合わせることが重要です。
セキュリティレイヤーの実装
| レイヤー | 対策 | 実装例 | 効果 |
|---|---|---|---|
| ネットワーク | アクセス制限、暗号化 | VPC、ファイアウォール、VPN | 外部攻撃の防止 |
| 認証 | 強力な認証メカニズム | mTLS、SASL、OAuth 2.0 | 不正アクセス防止 |
| 認可 | 最小権限の原則 | RBAC、ACL、属性ベース制御 | 権限昇格防止 |
| アプリケーション | 入力検証、サニタイゼーション | スキーマ検証、型チェック | インジェクション防止 |
| データ | 暗号化、マスキング | 保存時・通信時暗号化 | データ漏洩時の被害軽減 |
| 監査 | ログ記録、監視 | 全操作の記録、異常検知 | インシデントの早期発見 |
設定管理とハードニング
デフォルト設定のままでは多くのセキュリティリスクが存在するため、適切なハードニングが必要です。
- デフォルト設定の変更
- デフォルトポートの変更(MongoDB: 27017→カスタムポート、Redis: 6379→カスタムポート)により、自動化された攻撃を回避します。デフォルトユーザーは必ず削除し、独自の管理者アカウントを作成します。認証は必ず有効化し、可能な限り強力な認証メカニズム(SCRAM-SHA-256、証明書ベース認証等)を使用します。バインドアドレスをlocalhostから必要最小限のIPアドレスに制限することも重要です。
- 不要機能の無効化
- JavaScript実行機能(MongoDB)、Luaスクリプト(Redis)、HTTP API、リモート管理機能など、使用しない機能は必ず無効化します。特に、開発用の機能やデバッグ機能は本番環境では完全に無効化すべきです。危険なコマンド(FLUSHALL、CONFIG SET等)も、ACLやコマンドリネーミングで使用不可にします。
- 定期的な更新
- セキュリティパッチは公開後速やかに適用します。少なくとも月1回はセキュリティアドバイザリを確認し、四半期ごとにメジャーバージョンアップを検討します。更新前には必ず検証環境でテストを行い、ロールバック手順を準備しておくことが重要です。
NoSQL向けWAFとセキュリティツール
NoSQLデータベース専用のセキュリティソリューションを活用することで、より効果的な防御が可能です。
専用セキュリティソリューション
主要製品の比較
| 製品 | 対応DB | 主要機能 | 価格(月額) | 特徴 |
|---|---|---|---|---|
| DataDog Database Monitoring | MongoDB, Redis, Elasticsearch, Cassandra | パフォーマンス監視、異常検知、クエリ分析 | $15/host | APM統合、機械学習ベース |
| Imperva Data Security | MongoDB, Redis, CouchDB | WAF、データマスキング、監査 | 要見積 | エンタープライズ向け |
| ScaleGrid | MongoDB, Redis | 自動バックアップ、暗号化、監視 | $30~ | マネージドサービス |
| MongoDB Atlas | MongoDB専用 | 統合セキュリティ、LDAP、暗号化 | $57~ | 公式クラウドサービス |
| Redis Enterprise | Redis専用 | ACL、TLS、監査ログ | $71~ | エンタープライズ機能 |
移行とレガシーシステムの対策
既存の脆弱なNoSQLシステムを段階的にセキュアな状態へ移行する戦略が重要です。
段階的なセキュリティ強化
優先順位付けアプローチ
# NoSQLセキュリティ強化ロードマップ
Phase_1_緊急対策(1週間以内):
目標: 即座のリスク軽減
タスク:
- 認証の有効化:
MongoDB: "security.authorization: enabled"
Redis: "requirepass設定"
Elasticsearch: "xpack.security.enabled: true"
- ネットワークアクセス制限:
- ファイアウォール設定
- bindIpの制限(localhost or 特定IP)
- VPN/SSHトンネル経由のみ許可
- デフォルト設定の変更:
- デフォルトポート変更
- デフォルトユーザー削除
- サンプルデータ削除
検証:
- 外部からの接続テスト(失敗すること)
- 認証なしアクセステスト(拒否されること)
Phase_2_基本対策(1ヶ月以内):
目標: 基本的なセキュリティ確立
タスク:
- 入力検証の実装:
- 型チェック機能追加
- スキーマ検証導入
- サニタイゼーション処理
- 最小権限の適用:
- ユーザーロール定義
- データベース別アクセス制御
- 操作別権限設定
- ログ設定の有効化:
- 監査ログ有効化
- ログローテーション設定
- ログ監視ツール導入
- 暗号化の基本実装:
- TLS/SSL設定
- 通信の暗号化
成果物:
- セキュリティ設定書
- ユーザー権限マトリックス
- ログ監視ダッシュボード
Phase_3_高度対策(3ヶ月以内):
目標: 包括的セキュリティ体制
タスク:
- スキーマ検証の全面導入:
- JSON Schema定義
- バリデーションミドルウェア
- 型安全なORM/ODM導入
- 暗号化の完全実装:
- 保存時暗号化(Encryption at Rest)
- フィールドレベル暗号化
- キー管理システム導入
- 監視システムの構築:
- リアルタイム異常検知
- 自動アラート設定
- インシデント対応フロー
- バックアップとリカバリ:
- 自動バックアップ設定
- リストアテスト実施
- 災害復旧計画策定
成果物:
- セキュリティポリシー文書
- インシデント対応マニュアル
- 復旧手順書
Phase_4_継続的改善(継続実施):
目標: セキュリティの維持・向上
タスク:
- 定期診断の実施:
- 月次脆弱性スキャン
- 四半期ペネトレーションテスト
- 年次セキュリティ監査
- インシデント対応訓練:
- テーブルトップ演習
- 実機訓練
- 改善点の特定と対策
- 最新脅威への対応:
- セキュリティ情報収集
- パッチ適用プロセス
- 新機能のセキュリティ評価
- ドキュメント更新:
- 設定変更の記録
- 手順書の改訂
- ナレッジベース構築
KPI:
- 脆弱性発見から修正までの時間
- セキュリティインシデント数
- パッチ適用率
- セキュリティ教育受講率
よくある質問(FAQ)
- Q: NoSQLはSQLインジェクションがないから安全ですか?
- A: これは大きな誤解です。NoSQLデータベースには、SQLインジェクションとは異なる形式の深刻なインジェクション攻撃が存在します。MongoDBでは演算子インジェクション(`$ne`、`$gt`等)により認証バイパスが可能で、JavaScriptインジェクションではサーバー上で任意のコードを実行される危険があります。RedisではLuaスクリプトインジェクション、ElasticsearchではScript Injectionなど、各NoSQLデータベースには固有の脆弱性があります。むしろスキーマレスという特性により、SQLデータベースより予期しないデータ構造による攻撃を受けやすい面もあります。適切な入力検証、型チェック、最小権限の原則など、[SQLインジェクション対策](/security/web-api/sql-injection/)と同等以上の注意が必要です。
- Q: ORMやODMを使えばNoSQLインジェクションは防げますか?
- A: 部分的には防げますが、完全ではありません。MongooseなどのODM(Object Document Mapper)は、基本的なサニタイゼーションとスキーマ検証を提供し、多くの一般的な攻撃を防ぎます。しかし、開発者が`$where`句を直接使用したり、ユーザー入力を検証せずにクエリオブジェクトに含めた場合、依然として脆弱性が生じます。また、ODMのバージョンが古い場合や、設定が不適切な場合も危険です。ODM使用時でも、必ず入力の型チェック、演算子のフィルタリング、スキーマ定義の厳格化を行う必要があります。セキュリティは多層防御が基本であり、ODMは防御の一層に過ぎません。
- Q: クラウドマネージドサービスならセキュリティ対策は不要ですか?
- A: 基盤レベルのセキュリティは提供されますが、アプリケーション層の対策は依然として開発者の責任です。MongoDB Atlas、Amazon DynamoDB、Azure Cosmos DBなどのマネージドサービスは、ネットワークセキュリティ、保存時暗号化、自動パッチ適用、バックアップなどを提供します。しかし、インジェクション対策、入力検証、アクセス制御の実装、API キーの管理、データの分類と保護は利用者側で実施する必要があります。クラウドプロバイダーとの責任分界点を正確に理解し、アプリケーション層での[Webアプリケーションセキュリティ](/security/web-api/)対策を怠らないことが重要です。
- Q: JavaScriptを無効にすればMongoDBは安全になりますか?
- A: JavaScript実行の無効化は重要なセキュリティ対策の一つですが、それだけでは不十分です。`security.javascriptEnabled: false`の設定により、`$where`句やMapReduce機能でのコード実行は防げますが、演算子インジェクション(`$ne`、`$gt`等)による攻撃は依然として可能です。また、アプリケーション側でオブジェクトをそのままクエリに使用している場合、JSONインジェクションのリスクも残ります。包括的な対策として、JavaScript無効化に加えて、入力の型チェック、演算子のホワイトリスト化、最小権限の原則、監査ログの有効化なども必要です。
- Q: NoSQLデータベースの移行時にセキュリティはどう確保すべきですか?
- A: 段階的なアプローチで、リスクを最小化しながら移行することが重要です。まず移行前に現状のセキュリティ評価を実施し、脆弱性を把握します。次に、新環境では最初から認証有効、最小権限設定、暗号化有効の状態で構築します。データ移行は暗号化された経路で行い、移行ツールの脆弱性にも注意が必要です。並行運用期間を設け、両環境のセキュリティを維持しながら、段階的にトラフィックを切り替えます。移行完了後は、旧環境のデータを確実に削除し、新環境でのセキュリティ監査を実施します。全過程において、インシデント対応計画を準備しておくことも重要です。
まとめ
NoSQLインジェクションは、従来のSQLインジェクションとは異なる複雑な脅威であり、各NoSQLデータベースの特性に応じた対策が不可欠です。
NoSQLセキュリティの重要ポイント:
-
データベース固有の脆弱性理解
- MongoDB:演算子インジェクション、JavaScriptインジェクション
- Redis:コマンドインジェクション、Luaスクリプト実行
- Elasticsearch:Script Injection、クエリインジェクション
- 各DBの特性に応じた個別対策
-
多層防御アプローチの実装
- ネットワーク層:アクセス制限、暗号化通信
- 認証・認可層:強力な認証、最小権限の原則
- アプリケーション層:入力検証、型チェック、スキーマ検証
- データ層:暗号化、マスキング
-
包括的な入力検証
- 型チェックの徹底(文字列 vs オブジェクト)
- 演算子のフィルタリング
- JSON Schemaによる構造検証
- プロトタイプ汚染対策
-
設定のハードニング
- デフォルト設定の変更
- 不要機能の無効化(JavaScript実行等)
- ACLによるきめ細かいアクセス制御
- 定期的なセキュリティ更新
-
継続的な監視と改善
- リアルタイム異常検知
- 定期的な脆弱性診断
- インシデント対応体制の確立
- セキュリティ教育の実施
NoSQLデータベースの柔軟性と拡張性は大きな利点ですが、その特性ゆえのセキュリティリスクも存在します。「NoSQLだからSQLインジェクションは関係ない」という誤解を捨て、NoSQL固有の脅威に対する適切な対策を実装することが重要です。
最後に、セキュリティは一度実装すれば終わりではありません。新たな攻撃手法が日々開発される中、継続的な監視、定期的な診断、そして最新の脅威情報への対応が、真のセキュリティを実現する鍵となります。
【重要なお知らせ】
- 本記事は一般的な情報提供を目的としており、個別の状況に対する助言ではありません
- 各NoSQLデータベースの仕様は頻繁に更新されるため、公式ドキュメントで最新情報を確認してください
- セキュリティ設定の変更は、必ず検証環境でテストしてから本番環境に適用してください
- インジェクション攻撃のテストは、必ず自身が所有・管理するシステムでのみ実施してください
更新履歴
- 初稿公開