SJ blog
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)を導入
✅ 定期的な脆弱性スキャン

参考: OWASP SQL Injection / OWASP SQLi Prevention Cheat Sheet