SQL インジェクション
ユーザー入力を通じて不正な SQL 文を注入し、データベースを不正操作する攻撃手法
SQL インジェクションとは
SQL インジェクションは、ユーザー入力をそのまま SQL 文に埋め込む脆弱性を悪用し、攻撃者が任意の SQL を実行する攻撃手法である。データの窃取、改ざん、削除、認証バイパスに悪用される。
根本原因は、プログラムから見れば別物である「命令 (SQL の構文)」と「データ (入力値)」が、文字列連結で 1 本の文になった瞬間に区別できなくなることにある。データベースは届いた文字列を構文として解析するだけなので、入力値に混ざった引用符やコメント記号が構文の一部として働いてしまう。OWASP Top 10 では 2021 年版でインジェクションが A03 (3 位) へ下がったものの、代表的なカテゴリである点は変わっていない (2026 年 8 月時点)。
攻撃の仕組み
わかりやすいのは認証処理である。入力値を条件式へそのまま差し込むと、攻撃者は値ではなく条件式そのものを書き換えられる。
// ❌ 脆弱なコード: ユーザー入力を直接 SQL に埋め込み
const query = `SELECT * FROM users WHERE email = '${email}' AND password = '${password}'`;
// 攻撃者の入力: email = "' OR '1'='1' --"
// 生成される SQL:
// SELECT * FROM users WHERE email = '' OR '1'='1' --' AND password = ''
// → 全ユーザーが返る (認証バイパス)
-- は SQL のコメントで、以降の条件が無視される。'1'='1' は常に true なので、全レコードが返る。
攻撃の種類
画面に結果が出るかどうかで手口は変わる。結果が直接返らなくても、応答の真偽差や応答時間の差だけで 1 文字ずつ絞り込めるため、「データを画面に出していないから安全」とは言えない。
| 種類 | 手法 | 危険度 |
|---|---|---|
| Classic | UNION SELECT で他テーブルのデータを取得 | 高い |
| Blind | true/false の応答差でデータを推測 | 高い |
| Time-based Blind | SLEEP() の応答時間でデータを推測 | 高い |
| Second-order | 保存されたデータが後で SQL に使われる | 中程度 |
対策: パラメータ化クエリ (最重要)
OWASP の SQL インジェクション予防チートシートは、第 1 の防御としてプレースホルダを使ったプリペアドステートメント (パラメータ化クエリ) を挙げている。開発者に SQL の構文を先に確定させ、値を後から渡させる形にすることで、データベースが常に構文と値を区別できるようになるからである。
// ✅ パラメータ化クエリ: ユーザー入力は値として扱われ、SQL 構文として解釈されない
const result = await db.query(
'SELECT id, password_hash FROM users WHERE email = $1',
[email]
);
// パスワードの照合を SQL の条件式でやってはいけない。ソルト付きの
// ハッシュは同じパスワードでも保存値が変わるため、値の一致では照合できない。
// 取り出した password_hash とアプリ側で検証する (例: await verify(hash, password))
// DynamoDB: パラメータ化が標準
await ddb.send(new QueryCommand({
TableName: 'Users',
KeyConditionExpression: 'email = :email',
ExpressionAttributeValues: { ':email': email },
}));
パラメータ化クエリでは、ユーザー入力が SQL の構文として解釈されることはない。' OR '1'='1' -- は単なる文字列値として扱われる。
ただしプレースホルダに渡せるのは値だけである。テーブル名・列名・並び順の指定 (ASC / DESC) のような識別子やキーワードは値として渡せないので、ここに入力を反映したい場合は許可リスト方式 (あらかじめ決めた候補との照合) か問い合わせ自体の設計変更で組む。OWASP は、そもそもこの位置に利用者の入力値を持ってくること自体が設計の不備の兆候だとしている。
入力値をエスケープ関数で無害化する方式は、OWASP が「強く非推奨」として最後の選択肢に置いている。エスケープの規則はデータベース製品ごとに異なり壊れやすく、すべての SQL インジェクションを防げるとは保証できないためである。既存コードをパラメータ化へ書き換えられない場合の緊急避難であって、新規に選ぶ方式ではない。
ORM を使う
ORM のクエリビルダー経由で書けば、値は自動的にパラメータとして渡される。
// Prisma: パラメータ化が自動
const user = await prisma.user.findUnique({ where: { email } });
// Drizzle ORM
const user = await db.select().from(users).where(eq(users.email, email));
ORM は内部的にパラメータ化クエリを生成するため、SQL インジェクションのリスクが大幅に低減される。危ないのは生 SQL へ降りるときで、Prisma を例にとると挙動が 2 系統に分かれる (2026 年 8 月時点)。
$queryRaw/$executeRaw: テンプレートリテラルに埋めた変数がプリペアドステートメントの値として渡るため、素直に書けば安全である。ただし変数で識別子 (列名・テーブル名) や SQL のキーワードは差し替えられず、Prisma.rawを使って外部入力を文に混ぜると穴が開く。$queryRawUnsafe/$executeRawUnsafe: 完成した文字列を渡す形なので、入力を連結した時点で脆弱になる。値は$1や?の位置パラメータとして別引数で渡す (この形なら変数はエスケープされる)。
つまり判断基準は「ORM を使っているか」ではなく「文字列連結で SQL を組んでいないか」である。
多層防御
パラメータ化クエリで穴を塞いだうえで、被害を小さくする層を重ねる。順序を入れ替えてはいけない (2 つ目以降は 1 つ目の代わりにならない)。
- パラメータ化クエリ (必須・脆弱性そのものを無くせるのはこれだけ)
- 入力バリデーション (メールアドレスの形式チェックなど。プレースホルダを使えない識別子の箇所では、許可リストが本命の防御になる)
- 最小権限の DB ユーザー (アプリ用ユーザーに DROP 権限や管理者相当の権限を与えない。侵入されたときの被害範囲を狭めるだけで、脆弱性は残ったままである)
- エラーメッセージの制限 (DB のエラー詳細をユーザーに返さない。攻撃者の手間を増やすが、Blind 型の推測は成立する)
- WAF (AWS WAF のマネージドルールグループ
AWSManagedRulesSQLiRuleSetなど)
WAF を対策の中心に据えるのは危険である。OWASP の予防チートシートは第 1 防御に WAF を挙げておらず、迂回手法そのものが解説の対象になっている。実装上の限界もはっきりしていて、AWS WAF がリクエスト本文を検査できるのは本文サイズの上限までである (2026 年 8 月時点で ALB と AppSync は 8 KB 固定、CloudFront や API Gateway などは既定 16 KB で最大 64 KB まで引き上げ可)。上限を超えた部分は検査されないため、大きな JSON 本文の後ろに注入文字列を潜ませる余地が残る。WAF は既知パターンの機械的な遮断と、修正が終わるまでの時間稼ぎに位置づける。
DynamoDB でも「文字列連結しない」は同じ
DynamoDB のネイティブ API (Query / Scan) は SQL 文を組み立てないため、従来型の SQL インジェクションは起きない。ただし KeyConditionExpression や FilterExpression へユーザー入力を直接埋め込めば、条件式を書き換えられる同型のリスクがある。値は ExpressionAttributeValues に分離して渡す。
さらに DynamoDB は PartiQL (SQL 互換のクエリ言語) にも対応しており、SELECT / INSERT / UPDATE / DELETE をマネジメントコンソール・CLI・DynamoDB の API から実行できる (対応範囲は SQL の部分集合)。したがって「DynamoDB なら SQL 文は存在しない」とは言えない。PartiQL の文を文字列連結で組み立てれば、リレーショナルデータベースとまったく同じ構図の注入が成立する。ExecuteStatement は文と Parameters を分けて渡せるので、値は必ずパラメータ側に置く。
既存のコードベースを点検するときは、SQL 文が文字列連結・テンプレートリテラル・書式指定関数のいずれかで組み立てられている箇所を機械的に洗い出すのが早い。該当箇所を ORM のクエリビルダーかプレースホルダへ寄せ、どうしても動的にしたい識別子だけを許可リストへ落とす。この 2 手で大半は片付き、残る例外は件数が少ないのでレビューで守り切れる。
この記事は役に立ちましたか?