PHPで安全なSQLを!PDOのプレースホルダと値のバインド方法

詳細ページを作成するには、SELECT * FROM reviews WHERE id = 5のように、URLから受け取った特定のidを持つレコードをデータベースから取得する必要があります。

このとき、$id = $_GET['id']; $sql = "SELECT * FROM reviews WHERE id = $id"; のように、受け取った変数を直接SQL文に埋め込むのは絶対に行ってはいけません。これはSQLインジェクションという、Webアプリケーションで最も深刻な脆弱性の一つを生み出す、非常に危険な書き方です。

この記事では、この危険を回避し、安全にデータベースを操作するための現代的な手法である「プリペアドステートメント」と「プレースホルダ」の使い方を解説します。


目次

1. なぜプレースホルダが必要なのか?SQLインジェクションの脅威

SQLインジェクションとは、攻撃者が入力フォームやURLパラメータに不正なSQL文の一部を紛れ込ませることで、データベースを不正に操作する攻撃です。

もし変数を直接SQL文に埋め込むと、データベースはどこまでが本来のSQLで、どこからがユーザーの入力値なのかを区別できません。これにより、データの改ざん、情報漏洩、データの全削除といった壊滅的な被害に繋がる可能性があります。

プレースホルダ」は、この問題を解決するための仕組みです。SQL文のテンプレート(雛形)と、そこにはめ込む実際の「値」を完全に分離してデータベースに伝えることで、ユーザーの入力値をただの「データ」として扱うよう強制し、不正なSQLとして実行されるのを防ぎます。


2. プレースホルダを使ったSQLの準備 (prepare)

PDOでプリペアドステートメントを利用するには、まずprepare()メソッドでSQL文のテンプレートを準備します。このとき、後から値をはめ込みたい部分をプレースホルダに置き換えます。

プレースホルダには2種類の書き方があります。

  • ? (クエスチョンマークプレースホルダ): 名前のないプレースホルダ。順番が重要になります。PHP$sql = "SELECT * FROM reviews WHERE id = ?"; $stmt = $pdo->prepare($sql);
  • :name (名前付きプレースホルダ): :idのようにコロンから始まる名前を付けます。パラメータが多い場合に、どのプレースホルダにどの値が入るのかが分かりやすくなります。PHP$sql = "SELECT * FROM reviews WHERE rating >= :rating AND genre = :genre"; $stmt = $pdo->prepare($sql);

3. プレースホルダに値を渡す(バインドする)方法

準備したSQL文のプレースホルダに実際の値を渡す(バインドする)方法は、主に2つあります。

方法1(推奨): execute()に配列を渡す

最もシンプルで一般的な方法です。execute()メソッドの引数に配列を渡すことで、プレースホルダに値をバインドできます。

  • ?プレースホルダの場合: ?の順番に対応する値を入れた、添字配列を渡します。PHP$id = 5; $stmt->execute([$id]);
  • 名前付きプレースホルダの場合: プレースホルダ名をキーとする連想配列を渡します。PHP$stmt->execute([ ':rating' => 4, ':genre' => 'ビジネス' ]);

方法2(明示的): bindValue()を使う

execute()の前にbindValue()メソッドを使って、一つずつ値をバインドする方法もあります。この方法では、値のデータ型を明示的に指定することができます。

  • bindValue(プレースホルダ名/位置, 値, データ型)
$id = 5;

// ? プレースホルダの場合(1番目の?に値をバインド)
$stmt->bindValue(1, $id, PDO::PARAM_INT);

// 名前付きプレースホルダの場合
// $stmt->bindValue(':id', $id, PDO::PARAM_INT);

$stmt->execute();

PDO::PARAM_INTは値が整数であることを、PDO::PARAM_STRは文字列であることを示します。型を明示することで、より厳密な処理が可能になります。


実装例:IDを指定して特定のレビューを取得する

それでは、URLから受け取ったIDを安全に使い、特定のレビュー1件を取得する、実践的なコードを見てみましょう。

detail.phpのサンプルコード:

<?php
// データベース接続情報
$dsn = 'mysql:host=localhost;dbname=my_first_db;charset=utf8mb4';
$user = 'root';
$password = 'root';

try {
    // IDのバリデーション
    if (empty($_GET['id']) || !ctype_digit($_GET['id'])) {
        throw new Exception('IDが不正です。');
    }
    $id = (int)$_GET['id'];

    // データベースに接続
    $pdo = new PDO($dsn, $user, $password);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

    // プレースホルダを用いたSQLを準備
    $sql = "SELECT * FROM reviews WHERE id = ?";
    $stmt = $pdo->prepare($sql);

    // execute()の引数に配列で値を渡す
    $stmt->execute([$id]);

    // 結果を1件だけ連想配列として取得
    $review = $stmt->fetch(PDO::FETCH_ASSOC);

    // 結果が存在しない場合の処理
    if (!$review) {
        echo "指定されたIDのレビューは見つかりませんでした。";
        exit();
    }

    // 取得したデータを安全に出力
    echo "<h1>" . htmlspecialchars($review['book_title'], ENT_QUOTES, 'UTF-8') . "</h1>";
    echo "<p>評価: " . htmlspecialchars($review['rating'], ENT_QUOTES, 'UTF-8') . "</p>";
    echo "<div>" . nl2br(htmlspecialchars($review['review_text'], ENT_QUOTES, 'UTF-8')) . "</div>";

} catch (Exception $e) {
    echo "エラー: " . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8');
    die();
}
?>

まとめ

  • ユーザー入力などの変動する値をSQLに含める際は、絶対に直接変数を埋め込んではいけない
  • **prepare()でSQLのテンプレートを準備し、変動する部分はプレースホルダ (? or :name)**にする。
  • **execute()に配列を渡すか、bindValue()**を使って、安全に値をプレースホルダに渡す(バインドする)。

この「プリペアドステートメント」は、PHPでデータベースを扱う上での最も重要なセキュリティ対策です。必ずマスターして、安全なWebアプリケーションを開発しましょう。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次