SQLインジェクションは数十年の認識がありながら、今なおOWASP Top 10にランクインしています。2023〜2024年には医療、eコマース、政府機関でSQLiを悪用した重大な情報漏洩が発生しました。古典的な' OR 1=1 --攻撃はよく知られていますが、二次注入、ブラインドSQLi(時間ベース・真偽値ベース)、帯域外抽出などの最新亜種も増加しています。予防策は周知されているにもかかわらず、レガシーコード、ORMの誤用、テスト自動化の不足により実行が不十分なのが現状です。
パラメータ化クエリとプリペアドステートメント
プリペアドステートメントはSQLインジェクション対策の黄金基準です。SQLロジックとデータをデータベースエンジンレベルで分離し、ユーザー入力がクエリ構造を改変することを防ぎます。
// Node.js (pg) — 安全
const result = await client.query(
"SELECT * FROM users WHERE email = $1 AND status = $2",
[userEmail, "active"]
);
// Python (psycopg2) — 安全
cursor.execute(
"INSERT INTO orders (user_id, amount) VALUES (%s, %s)",
[user_id, amount]
);
文字列補間は、入力が「エスケープ」されていても決して安全ではありません。パラメータ化クエリはクエリプランキャッシュを損なわず、むしろ多くのデータベースでアドホックSQLよりも効率的にキャッシュされます。
| 言語 | 安全なAPI | 危険なパターン |
|---|---|---|
| Node.js (pg) | client.query(sql, params) | 文字列補間によるSQL |
| Python (psycopg2) | cursor.execute(sql, params) | f-strings や % 書式 |
| Java (JDBC) | PreparedStatement | Statement + 文字列結合 |
| C# (SqlClient) | SqlCommand + Parameters | インラインSQL |
| PHP (PDO) | PDO::prepare() + execute() | mysqli::query() + 連結 |
動的テーブル名や可変長IN句は注意が必要です。テーブル名は許可リストで検証し、動的リストにはANY配列構文を使用します。
ORM保護と落とし穴
Prisma、TypeORM、Django ORM、HibernateなどのORMはデフォルトでインジェクションを防止します。しかし、生クエリ、LIKE句、ORDER BY句でリスクが再発します。
// Prisma — 生クエリの落とし穴
const users = await prisma.$queryRawUnsafe(
`SELECT * FROM users WHERE name LIKE '%${searchTerm}%'` // 脆弱
);
// 安全な代替:パラメータバインディング
const users = await prisma.$queryRaw`
SELECT * FROM users WHERE name LIKE ${"%" + searchTerm + "%"}
`;
ORDER BY句はパラメータ化できません。カラム名を許可リストで検証してください:
const allowedColumns = ["name", "email", "created_at"];
if (!allowedColumns.includes(sortBy)) {
throw new Error("無効なソートカラム");
}
const result = await client.query(
`SELECT * FROM users ORDER BY ${sortBy} ${sortDir}`,
);
ORMは銀の弾丸ではありません — 開発者は基盤となるSQLの振る舞いを理解する必要があります。
ストアドプロシージャとデータベース強化
適切にパラメータ化されたストアドプロシージャは安全です。しかし、内部でEXECやEXECUTE IMMEDIATEを使用した動的SQLはリスクを再導入します。
-- 安全
CREATE PROCEDURE get_user(IN user_id INT)
BEGIN
SELECT * FROM users WHERE id = user_id;
END;
-- 脆弱
CREATE PROCEDURE get_user(IN table_name VARCHAR(64))
BEGIN
SET @sql = CONCAT('SELECT * FROM ', table_name);
PREPARE stmt FROM @sql;
EXECUTE stmt;
END;
最小権限の原則を適用します。アプリケーションのDBユーザーには必要最小限の権限のみ付与し、DROP、ALTER、CREATEは許可しません。PostgreSQLの行レベルセキュリティ、SQL ServerのEXECUTE AS、MySQLのSQL SECURITYを活用してデータベース層でアクセスを制御します。
入力検証とWAF
入力検証だけでは不十分ですが、第二層として不可欠です。ブラックリスト方式よりホワイトリスト方式を優先します。型強制(parseIntによる整数変換)、長さ制限、Unicode正規化(NFC/NFD)により多くのバイパス試行を防げます。
WAFは遅延戦術であり、解決策ではありません。攻撃者はコメント難読化(/**/)、エンコーディング亜種(URL、Unicode、Hex、二重URLエンコード)、HTTPパラメータ汚染、SLEEP()の代わりにBENCHMARK()を使用するなどの手法でWAFを回避します。
-- WAF回避:コメント難読化
' UNION/**/SELECT/**/password/**/FROM/**/users--
-- WAF回避:別の関数を使用
1 AND BENCHMARK(5000000, MD5('test'))
NoSQLインジェクションと自動スキャン
NoSQLデータベースもインジェクションに対して脆弱です。MongoDBの$where句注入、$regex注入、JSONクエリオブジェクト操作は全てリスクです。原則は普遍的です:ユーザー入力を連結するデータベース言語はすべて脆弱です。
| データベース | インジェクションベクター | 防御策 |
|---|---|---|
| MongoDB | $where, $regex, $ne | $eqの使用、演算子の検証 |
| Couchbase | N1QL文字列連結 | パラメータ化N1QL |
| Cassandra | CQL文字列連結 | プリペアドステートメント |
| Redis | EVAL/EVALSHA (Lua) | パラメータ化EVAL |
シフトレフトで自動スキャンを導入します。Semgrep、ESLintセキュリティプラグイン、CodeQLなどのSASTツールがプルリクエストでインジェクションパターンを検出します。OWASP ZAPなどのDASTツールが実行中のアプリケーションを動的にスキャンします。
name: Security Scan
on: pull_request
jobs:
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npx semgrep --config=auto
防止が失敗した場合に備えて、異常なクエリパターン、予期しないエラー、データ送信量の急増を監視します。資格情報を即座にローテーションし、クエリログ分析で注入箇所を特定し、データ漏洩を封じ込め、フォレンジック証拠を保存します。
SQLインジェクションは、確立されたパターンを一貫して適用することで完全に防止可能です。パラメータ化クエリを一次防御とし、ORM(生クエリ認識あり)を構造的防御とし、入力検証とWAFを二次層とし、CI/CDの自動スキャンを検証手段とします。これらの層を一貫して適用することで、脆弱性のクラス全体を排除できます。
