# ch09 — SQL インジェクション / プリペアド vs 連結

Source: https://php-school.pages.dev/topics/db/ch09-sql-injection/
Site: AIと学ぶPHPの学校 (https://php-school.pages.dev)
Lesson: データベース連携
Level: L4

Reference (official): https://www.php.net/manual/ja/book.pdo.php

## 本文

# ch09 — SQL インジェクション / プリペアド vs 連結

## 概要

ch04 以降ずっと「**値はプレースホルダで渡す**」と繰り返してきた理由を、この章で実弾を撃って確認する。**SQL インジェクション** とは、ユーザー入力に SQL の構文要素 (`'` や `;` や `--`) を紛れ込ませて、開発者の意図しない SQL を **DB に実行させる** 攻撃で、Web アプリの最古典の脆弱性。情報漏洩・改ざん・テーブル破壊と被害幅も広い。

仕組みは単純で、`"WHERE name = '$name'"` のように **文字列連結** で SQL を組み立てていると、入力 `' OR '1'='1` を埋めた瞬間に条件が常に真になり **全件返ってくる**。`'; DROP TABLE users; --` を紛れ込ませれば、テーブルを消されることすらある。

これに対するプレースホルダ (`prepare` + `execute`) は、SQL の **形** と **値** を **別ルート** で DB に送る方式。DB は最初に「形」をパースして実行計画を作り、後から来た「値」は **常にデータとして扱う**。だから `'` も `;` も SQL 構文として解釈されない。ドリルは危険な連結版 starter をプレースホルダ版に書き直して、両者の違いを手で覚える。

## 学習目標

- 文字列連結で SQL を組み立てる危険性を体感する
- プレースホルダ (`?`) + `prepare/execute` がなぜ安全なのか、その理由を言える
- ユーザー入力に特殊文字 (`'` や `;`) が混ざっても壊れない書き方が分かる

## 所要時間

スライド 5 分 + ドリル 2 問 = **18 分**

## ドリル

| no | 内容 |
|---|---|
| 01 | 文字列連結で書かれた starter を、プレースホルダ版に直す (`'` 入りの名前で動作確認) |
| 02 | 検索フォーム想定。LIKE 検索でもプレースホルダを使う |

## 採点用 DB について

採点ランナーは `tests/setup.sql` を一時 SQLite に流し込み、そのファイルパスを環境変数 `DOJO_DB_PATH` で渡してきます。受講生コードは次の形で接続してください。

```php
$pdo = new PDO('sqlite:' . getenv('DOJO_DB_PATH'));
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
```

## なぜ文字列連結が危険か

```php
// ❌ 危険: 入力をそのまま SQL に連結
$name = $_GET['name'];
$sql = "SELECT * FROM users WHERE name = '$name'";
```

ユーザーが `name=' OR '1'='1` を入れると、SQL は

```sql
SELECT * FROM users WHERE name = '' OR '1'='1'
```

になり、**全件返ってくる**。さらに悪意ある入力で `'; DROP TABLE users; --` を渡されると、テーブル自体を壊される可能性がある。

プレースホルダ版なら、入力は **値** として扱われるので SQL 構文として解釈されない。

## スライド

---
marp: true
theme: dojo
paginate: true
size: 16:9
---

<!--
LLM_CONTEXT:
  Lesson 11 / Chapter 9
  目的: SQL インジェクションの原理と、プレースホルダで防ぐ仕組みを身体で理解する
  扱わない: WHERE 句の文法 (ch04) / INSERT (ch05) / トランザクション (ch08) / 複数 DB 接続 (ch10)
  読み上げ時間目安: 4 分半〜5 分
-->

# SQL インジェクション / プリペアド vs 連結

Lesson 11 / Chapter 9

---

## 文字列連結で SQL を組み立てると壊れる

```php
<?php
$name = $_GET['name'];                              // ユーザー入力
$sql  = "SELECT * FROM users WHERE name = '$name'"; // ❌ そのまま連結
$pdo->query($sql);
```

入力が普通の名前ならよさそうに見える。

問題は、入力が `' OR '1'='1` のとき:

```sql
SELECT * FROM users WHERE name = '' OR '1'='1'
```

→ **全件返ってくる**。ログイン処理でこれをやると突破される。

---

## さらに悪意のある入力

入力が `'; DROP TABLE users; --` だったら:

```sql
SELECT * FROM users WHERE name = ''; DROP TABLE users; --'
```

- `;` で SQL 文を区切られ、`DROP TABLE users` が **続けて実行される**
- `--` 以降はコメント扱いで無効化される

これが **SQL インジェクション**。データ全削除やパスワード抜き取りに使われる典型攻撃。

---

## プレースホルダで防ぐ

```php
<?php
$stmt = $pdo->prepare("SELECT * FROM users WHERE name = ?");
$stmt->execute([$name]);
```

- `?` の部分は **値の入る場所** として SQL に渡される
- `$name` は **値** として扱われ、SQL 構文として解釈されない
- 入力に `'` や `;` が混じっていても、ただの文字として比較される

→ `' OR '1'='1` という名前のユーザーを探しに行くだけ (誰もいないので 0 件)。

---

## prepare / execute の流れ

```php
<?php
// 1. SQL の「型」を DB に登録 (値の場所だけ ? にする)
$stmt = $pdo->prepare("SELECT id, name FROM users WHERE name = ?");

// 2. 値を渡して実行
$stmt->execute([$name]);

// 3. 結果を取り出す
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
    echo "{$row['id']}: {$row['name']}\n";
}
```

ポイント: `prepare` した時点で SQL の構造は確定済み。後から値を渡しても **構造を書き換えられない**。

---

## LIKE 検索でも同じルール

部分一致検索 (`name LIKE '%山%'`) でも、`%` はクエリ側で作って、入力は値として渡す。

```php
<?php
$keyword = trim(fgets(STDIN));
$stmt = $pdo->prepare("SELECT id, name FROM users WHERE name LIKE ?");
$stmt->execute(['%' . $keyword . '%']);  // % は PHP 側で付ける
```

- 連結は SQL の中ではなく、**プレースホルダに渡す値の側** で行う
- これなら `keyword` に `'` や `;` が入っても安全

---

## このチャプターでできるようになること

✅ 文字列連結が危険な理由を `' OR '1'='1` の例で説明できる
✅ `prepare + ?` で SQL の構造と値を分離する書き方ができる
✅ LIKE 検索でも、`%` は SQL 側に書いて値だけプレースホルダに渡せる
✅ 「ユーザー入力を SQL に連結したら危険」という反射を持てる

→ ドリルへ
