ch09 12章中 9章目

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

スライドで見る

この章を AI と学ぶ

💬 AI に聞く質問例 3 件
  1. 「ch09 — SQL インジェクション / プリペアド vs 連結」のつまずきやすいポイントを、初学者向けに3つ教えてください

    ChatGPT Claude Gemini

  2. 「ch09 — SQL インジェクション / プリペアド vs 連結」について、自分の手で試せる練習問題を3つ出してください (難易度を上げながら)

    ChatGPT Claude Gemini

  3. 「ch09 — SQL インジェクション / プリペアド vs 連結」を学んだ後、次に学ぶと良いトピックは何ですか?理由も教えてください

    ChatGPT Claude Gemini

概要

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 がなぜ安全なのか、その理由を言える
  • ユーザー入力に特殊文字 (';) が混ざっても壊れない書き方が分かる

ドリル

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

採点用 DB について

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

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

なぜ文字列連結が危険か

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

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

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

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

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

解説スライド 全 7 枚

右の ボタン キー(またはスワイプ)で次のスライドに進めます。 下の「縦表示」を開くと全スライドを一気に読めます。

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

    Lesson 11 / Chapter 9

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

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

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

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

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

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

  3. さらに悪意のある入力

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

    SELECT * FROM users WHERE name = ''; DROP TABLE users; --'
    • ; で SQL 文を区切られ、DROP TABLE users続けて実行される
    • -- 以降はコメント扱いで無効化される

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

  4. プレースホルダで防ぐ

    <?php
    $stmt = $pdo->prepare("SELECT * FROM users WHERE name = ?");
    $stmt->execute([$name]);
    • ? の部分は 値の入る場所 として SQL に渡される
    • $name として扱われ、SQL 構文として解釈されない
    • 入力に '; が混じっていても、ただの文字として比較される

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

  5. prepare / execute の流れ

    <?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 の構造は確定済み。後から値を渡しても 構造を書き換えられない

  6. LIKE 検索でも同じルール

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

    <?php
    $keyword = trim(fgets(STDIN));
    $stmt = $pdo->prepare("SELECT id, name FROM users WHERE name LIKE ?");
    $stmt->execute(['%' . $keyword . '%']);  // % は PHP 側で付ける
    • 連結は SQL の中ではなく、プレースホルダに渡す値の側 で行う
    • これなら keyword'; が入っても安全
  7. このチャプターでできるようになること

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

    → ドリルへ

1 / 7
📜 スライドを全部一気に読む(縦表示)

Lesson 11 / Chapter 9


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

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

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

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

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

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


さらに悪意のある入力

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

SELECT * FROM users WHERE name = ''; DROP TABLE users; --'
  • ; で SQL 文を区切られ、DROP TABLE users続けて実行される
  • -- 以降はコメント扱いで無効化される

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


プレースホルダで防ぐ

<?php
$stmt = $pdo->prepare("SELECT * FROM users WHERE name = ?");
$stmt->execute([$name]);
  • ? の部分は 値の入る場所 として SQL に渡される
  • $name として扱われ、SQL 構文として解釈されない
  • 入力に '; が混じっていても、ただの文字として比較される

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


prepare / execute の流れ

<?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
$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 に連結したら危険」という反射を持てる

→ ドリルへ

この章で取り組む 演習問題の流れ (2ステップ)
  1. 01 ドリル 01 — 連結をプレースホルダに直す README.md starter.php answer.php
  2. 02 ドリル 02 — LIKE 検索もプレースホルダで README.md starter.php answer.php

演習問題の詳細

この章の演習問題の内容を読めます。実際に手元で解くには教材リポジトリを clone してください。

ドリル 01 — 連結をプレースホルダに直す

問題

starter.php はわざと 文字列連結 で SQL を組み立てている。これだと ' (シングルクォート) を含む名前を入れた瞬間に SQL 構文エラーで落ちる。

ターゲット名:

O'Brien

users テーブルに O'Brien (id=1) が登録されている。標準入力からこの名前を受け取り、

1: O'Brien

と出力するように プレースホルダ + prepare/execute で書き直してほしい。

期待される出力

1: O'Brien

ヒント

  • $pdo->prepare("SELECT id, name FROM users WHERE name = ?")? がプレースホルダ
  • $stmt->execute([$name]) で値を渡す
  • 連結 ("... WHERE name = '$name'") は禁止
  • PDO::ATTR_ERRMODEPDO::ERRMODE_EXCEPTION にする習慣を付けておく

🧪 ブラウザで試す

スライドのコードを写経して ▶ 実行、自分で書いて 📝 採点 で答え合わせ。初回のみ PHP ランタイム読込で数秒かかります。

PHP ランタイム未読込

          

        

ドリル 02 — LIKE 検索もプレースホルダで

問題

ユーザーが入力したキーワードで users.name部分一致検索 (LIKE) するコードを書きたい。

  • 標準入力からキーワードを 1 行受け取る
  • name にそのキーワードを含むユーザーを id 昇順で全件出力する
  • % (ワイルドカード) は PHP 側 で付け、プレースホルダには値だけ渡す

users テーブルには次が入っている (setup.sql 参照):

id name
1 山田太郎
2 鈴木花子
3 山本次郎
4 O'Brien

期待される入力

期待される出力

1: 山田太郎
3: 山本次郎

ヒント

  • WHERE name LIKE ? でプレースホルダ
  • 値の側で '%' . $keyword . '%' を作って渡す
  • SQL の中に '%$keyword%' のように埋め込むのは NG
  • ORDER BY id で並び順を固定

🧪 ブラウザで試す

スライドのコードを写経して ▶ 実行、自分で書いて 📝 採点 で答え合わせ。初回のみ PHP ランタイム読込で数秒かかります。

PHP ランタイム未読込

          

        

演習問題(2問)

  1. ドリル 01 — 連結をプレースホルダに直す

    README.md starter.php answer.php

  2. ドリル 02 — LIKE 検索もプレースホルダで

    README.md starter.php answer.php

サイト内で問題文・雛形・解答例を確認できます。実際に手元で解くには教材リポジトリを clone してください。

完了にすると進捗に反映されます