security
S
信頼度ランク
| S | 公式ソース確認済み |
| A | 成功実績多数・失敗例少数 |
| B | 賛否両論 |
| C | 動作未確認・セキュリティリスク高 |
| Z | 個人所感 |
SQLインジェクション完全解説と防御実装
SQLインジェクションの攻撃手法(古典的・Blind・Time-based)と、プリペアドステートメント・ORM・WAFによる防御を実例コードで解説。初心者から中級者まで理解できる内容です。
一言結論
SQLインジェクションはプリペアドステートメント一つで99%防げる古典的な脆弱性だが、今なお頻発するのは「文字列結合でSQLを組み立てる」コードが書かれ続けているからであり、ORMを使う場合もrawクエリ部分の確認は必須だ。
SQLインジェクションとは
ユーザー入力をSQL文に直接埋め込むことで、意図しないSQL文を実行させる攻撃です。
-- 正常なリクエスト
SELECT * FROM users WHERE email = 'alice@example.com'
-- 攻撃: email に " OR '1'='1'-- " を入力
SELECT * FROM users WHERE email = '' OR '1'='1'--'
-- → 全ユーザーが返ってくる('1'='1' は常に真、-- 以降はコメント)
攻撃の種類
古典的インジェクション(データ取得)
入力: ' UNION SELECT username, password FROM users--
結果: 元のクエリの代わりに users テーブルが返ってくる
Blind インジェクション(真偽で情報を推測)
-- 1文字ずつパスワードを推測
' AND SUBSTRING(password,1,1)='a'--
' AND SUBSTRING(password,1,1)='b'--
-- レスポンスが変わる文字 = 正解
Time-based Blind(タイミングで推測)
-- PostgreSQL の例: 条件が真なら5秒スリープ
' AND (SELECT CASE WHEN (1=1) THEN pg_sleep(5) ELSE pg_sleep(0) END)--
防御方法1: プリペアドステートメント(最重要)
// ❌ 文字列結合(危険)
const query = `SELECT * FROM users WHERE email = '${email}'`;
await db.query(query);
// ✅ プリペアドステートメント
await db.query("SELECT * FROM users WHERE email = $1", [email]);
// Node.js + postgres
const result = await client.query(
"SELECT id, name FROM users WHERE email = $1 AND is_active = $2",
[email, true]
);
# Python + psycopg2
cursor.execute(
"SELECT * FROM users WHERE email = %s AND role = %s",
(email, role) # タプルで渡す
)
// Java + JDBC
PreparedStatement stmt = conn.prepareStatement(
"SELECT * FROM users WHERE email = ?"
);
stmt.setString(1, email);
ResultSet rs = stmt.executeQuery();
防御方法2: ORM を使う
ORM は内部でプリペアドステートメントを使います。
// Prisma(TypeScript)
const user = await prisma.user.findFirst({
where: { email, isActive: true }
});
// → SELECT * FROM users WHERE email = ? AND is_active = ? と展開される
// SQLAlchemy(Python)
user = session.query(User).filter(
User.email == email,
User.is_active == True
).first()
ただし、生のSQL文字列を使う場合は同様に注意が必要です:
// ❌ Prisma でも raw クエリは危険
await prisma.$queryRawUnsafe(`SELECT * FROM users WHERE email = '${email}'`);
// ✅ $queryRaw でパラメータを渡す
await prisma.$queryRaw`SELECT * FROM users WHERE email = ${email}`;
防御方法3: 入力バリデーション
プリペアドステートメントが主防御ですが、入力検証も重要です。
import { z } from "zod";
const loginSchema = z.object({
email: z.string().email().max(255),
password: z.string().min(8).max(128),
});
app.post("/login", async (req, res) => {
const result = loginSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.issues });
}
// バリデーション済みのデータを使う
const { email, password } = result.data;
});
防御方法4: 最小権限の原則
-- アプリ用のDBユーザーは必要最小限の権限のみ
CREATE USER app_user WITH PASSWORD 'securepassword';
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE users TO app_user;
-- DROP TABLE, CREATE TABLE などは付与しない
脆弱性のテスト
# sqlmap で脆弱性を自動検出(自社システムのみで使用)
sqlmap -u "https://example.com/login" \
--data "email=test&password=test" \
--level=3 --risk=2
# OWASP ZAP でスキャン(GUIツール)
まとめ
SQLインジェクション防御のチェックリスト:
✅ すべてのDBクエリでプリペアドステートメントを使用
✅ ORM を使う場合も $queryRaw は避ける
✅ DB ユーザーには最小権限のみ付与
✅ 入力値のバリデーションを実装
✅ WAF(Web Application Firewall)を導入
✅ 定期的な脆弱性スキャン