LESSON 30分

パラメータ化クエリで防御しよう

ストーリー

「攻撃手法と脆弱性の見つけ方は分かった。ここからは防御だ」

高橋さんが言う。

「SQLインジェクション対策の最も効果的な方法は、パラメータ化クエリだ。 これを徹底するだけで、SQLインジェクションの99%を防げる」

「なぜそれほど効果的なんですか?」

「文字列連結では、ユーザー入力がSQL構文の一部として解釈される。 パラメータ化クエリでは、入力は常にデータとして扱われ、SQL構文に影響を与えない。 根本原因を断ち切る対策だからだ」


パラメータ化クエリとは

パラメータ化クエリ(Prepared Statement / プリペアドステートメント)は、SQL文の構造と値を分離する手法です。

仕組み

文字列連結(危険):
1. SQL文 = "SELECT * FROM users WHERE name = '" + 入力値 + "'"
2. データベースが SQL文全体 を解析
→ 入力値が SQL構文 として解釈される可能性がある

パラメータ化クエリ(安全):
1. SQL文のテンプレート = "SELECT * FROM users WHERE name = $1"
2. データベースがテンプレートを解析(構造が確定)
3. パラメータ(入力値)をデータとしてバインド
→ 入力値は常に 値(データ) として扱われる
┌─────────────────────────────────────┐
│ パラメータ化クエリの内部処理           │
│                                     │
│ ステップ1: SQL構造を解析              │
│   SELECT * FROM users WHERE name = ? │
│                                ^^^^  │
│                              プレースホルダ │
│                                     │
│ ステップ2: 値をバインド               │
│   ? ← "' OR 1=1 --"                │
│                                     │
│ ステップ3: 実行                      │
│   name = "' OR 1=1 --" (文字列として比較) │
│   → 一致するレコードなし(安全)       │
└─────────────────────────────────────┘

各言語・ライブラリでの実装

Node.js (pg / PostgreSQL)

typescript
// 脆弱なコード
const query = `SELECT * FROM users WHERE email = '${email}'`;
const result = await pool.query(query);

// セキュアなコード: $1, $2 ... でプレースホルダを指定
const query = 'SELECT * FROM users WHERE email = $1';
const result = await pool.query(query, [email]);

// 複数パラメータ
const query = 'SELECT * FROM users WHERE email = $1 AND status = $2';
const result = await pool.query(query, [email, status]);

// INSERT文
const query = 'INSERT INTO users (name, email, role) VALUES ($1, $2, $3) RETURNING id';
const result = await pool.query(query, [name, email, role]);

Node.js (mysql2)

typescript
// MySQL では ? をプレースホルダとして使用
const query = 'SELECT * FROM users WHERE email = ? AND status = ?';
const [rows] = await pool.execute(query, [email, status]);

Python (psycopg2 / PostgreSQL)

python
# 脆弱なコード
cursor.execute(f"SELECT * FROM users WHERE email = '{email}'")

# セキュアなコード: %s でプレースホルダを指定
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))

# 複数パラメータ
cursor.execute(
    "SELECT * FROM users WHERE email = %s AND status = %s",
    (email, status)
)

Python (SQLAlchemy)

python
from sqlalchemy import text

# text() を使ったパラメータ化クエリ
stmt = text("SELECT * FROM users WHERE email = :email AND status = :status")
result = session.execute(stmt, {"email": email, "status": status})

ORMを使った安全なデータベース操作

ORMを使用することで、SQLインジェクションのリスクをさらに低減できます。

Prisma(TypeScript)

typescript
// Prisma はデフォルトでパラメータ化クエリを使用
const user = await prisma.user.findUnique({
  where: { email: email }
});

// 検索
const users = await prisma.user.findMany({
  where: {
    name: { contains: searchTerm },
    status: 'active'
  }
});

// 作成
const newUser = await prisma.user.create({
  data: { name, email, role: 'user' }
});

// 更新
const updated = await prisma.user.update({
  where: { id: userId },
  data: { name: newName }
});

Prisma で raw query を使う場合の注意

typescript
// 安全: タグ付きテンプレートリテラル(Prisma が自動的にパラメータ化)
const users = await prisma.$queryRaw`
  SELECT * FROM users WHERE name LIKE ${`%${searchTerm}%`}
`;

// 危険: $queryRawUnsafe はパラメータ化しない
const users = await prisma.$queryRawUnsafe(
  `SELECT * FROM users WHERE name = '${searchTerm}'`  // SQLインジェクション可能
);

LIKE句のパラメータ化

LIKE句でのパラメータ化は少し注意が必要です。

typescript
// 脆弱: LIKE句に直接埋め込み
const query = `SELECT * FROM products WHERE name LIKE '%${search}%'`;

// セキュアだが不完全: ワイルドカードのエスケープが必要
const query = 'SELECT * FROM products WHERE name LIKE $1';
await pool.query(query, [`%${search}%`]);
// search に % や _ が含まれると意図しない動作になる

// 完全にセキュア: ワイルドカード文字もエスケープ
function escapeLikePattern(pattern: string): string {
  return pattern.replace(/[%_\\]/g, '\\$&');
}

const safeSearch = escapeLikePattern(search);
const query = "SELECT * FROM products WHERE name LIKE $1 ESCAPE '\\'";
await pool.query(query, [`%${safeSearch}%`]);

IN句のパラメータ化

typescript
// 脆弱: IN句に直接埋め込み
const ids = req.query.ids; // "1,2,3"
const query = `SELECT * FROM users WHERE id IN (${ids})`;

// セキュア: 各値を個別のパラメータとして渡す
const ids = [1, 2, 3];
const placeholders = ids.map((_, i) => `$${i + 1}`).join(', ');
const query = `SELECT * FROM users WHERE id IN (${placeholders})`;
await pool.query(query, ids);
// 生成されるSQL: SELECT * FROM users WHERE id IN ($1, $2, $3)

ORDER BY句の安全な処理

ORDER BY句はパラメータ化できないため、ホワイトリストで制御します。

typescript
// 脆弱: ORDER BY に直接埋め込み
const sortBy = req.query.sort; // "name; DROP TABLE users --"
const query = `SELECT * FROM users ORDER BY ${sortBy}`;

// セキュア: ホワイトリストで許可するカラムを制限
const ALLOWED_SORT_COLUMNS = ['name', 'email', 'created_at'];
const ALLOWED_SORT_DIRECTIONS = ['ASC', 'DESC'];

const sortBy = ALLOWED_SORT_COLUMNS.includes(req.query.sort)
  ? req.query.sort
  : 'created_at';

const sortDir = ALLOWED_SORT_DIRECTIONS.includes(req.query.dir?.toUpperCase())
  ? req.query.dir.toUpperCase()
  : 'DESC';

const query = `SELECT id, name, email FROM users ORDER BY ${sortBy} ${sortDir}`;
// sortBy と sortDir はホワイトリストで検証済みのため安全
await pool.query(query);

まとめ

ポイント内容
パラメータ化クエリSQL構文と値を分離し、入力を常にデータとして扱う
ORMデフォルトでパラメータ化されるため、さらに安全
LIKE句ワイルドカード文字のエスケープも必要
ORDER BYパラメータ化できないため、ホワイトリストで制御

チェックリスト

  • パラメータ化クエリの仕組みを理解した
  • 各言語でのパラメータ化クエリの書き方を理解した
  • ORMの安全性と raw query 使用時の注意点を理解した
  • LIKE句やORDER BY句の安全な処理方法を理解した

次のステップへ

パラメータ化クエリによるSQLインジェクション対策を学びました。 次のセクションでは、より広範な入力バリデーションとサニタイズについて学びます。


推定読了時間: 30分