PHP

PHPのPDOでSQLを安全に実行する方法|prepare・bindValue・executeを解説

PHPのPDOを使い、SQLインジェクションの具体例から、prepare・bindValue・executeによる安全なSQL実行、エラー処理までを実務向けに整理します。

この記事でわかること

  • SQL文にユーザー入力を文字列連結しない理由を理解できる
  • PDOのprepare・bindValue・executeの役割を整理できる
  • 値のバインドを使った安全なSQL実行手順を確認できる
  • 入力値の検証や例外処理など、プレースホルダー以外の注意点も把握できる

PDOを使う前に知っておきたいSQLインジェクションの危険性

検索語やログインIDなどの入力値をSQL文字列へ直接連結すると、入力内容がSQLの構文として解釈される可能性があります。たとえば、検証環境で次のような実装を使うと、入力値に条件を追加する文字列が含まれた場合に、意図しない検索結果になるおそれがあります。

PHP
<?php
$userName = $_GET['name'] ?? '';

// 危険な例:入力値をSQLへ直接連結しない
$sql = "SELECT id, name FROM users WHERE name = '$userName'";

// 例:検証環境で入力値にSQL構文が混ざると、条件が意図せず変わる可能性がある
// $userName = "' OR '1'='1";

安全な実装では、SQL文の構造とユーザー入力の値を分離します。PDOのプレースホルダーは、ユーザー入力をSQL文へ直接含めずに扱うための基本手段です。PHP公式マニュアルも、ユーザー入力をクエリへ直接含めず、パラメータマーカーを使うよう説明しています。

PDOのprepare・bindValue・executeの役割を整理する

PDOで安全にSQLを実行する基本の流れは、接続、prepare、値のバインド、executeです。prepareはプレースホルダーを含むSQL文を準備し、bindValueはプレースホルダーへ値と型を関連付けます。executeは準備済みのSQLを実行します。

bindValueを使わず、executeへ値の配列を渡す書き方もあります。PDO公式マニュアルでは、executeへ渡すパラメータは入力専用の値として扱われ、デフォルトではPDO::PARAM_STRとして扱われると説明されています。型を明示したい場合はbindValueが適しています。

prepareとbindValueで安全にSQLを実行する手順

次の例は、接続先、データベース名、テーブル構造を実環境に合わせて置き換える汎用例です。入力値をSQLへ連結せず、名前付きプレースホルダーへバインドしています。

PHP
<?php
$dsn = 'mysql:host=localhost;dbname=sample;charset=utf8mb4';
$dbUser = getenv('DB_USER');
$dbPassword = getenv('DB_PASSWORD');

try {
    $pdo = new PDO($dsn, $dbUser, $dbPassword, [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    ]);

    $name = $_GET['name'] ?? '';

    // SQLの構造と入力値を分離する
    $stmt = $pdo->prepare(
        'SELECT id, name FROM users WHERE name = :name'
    );

    $stmt->bindValue(':name', $name, PDO::PARAM_STR);
    $stmt->execute();

    $users = $stmt->fetchAll(PDO::FETCH_ASSOC);
    // 期待される挙動:条件に一致する行を配列として取得する
} catch (PDOException $e) {
    // 実運用では詳細な例外情報を画面へ直接表示しない
    error_log($e->getMessage());
    // 利用者向けには、環境に応じた一般的なエラーを返す
}

bindValueの第3引数にはPDO::PARAM_STRやPDO::PARAM_INTなどを指定できます。ただし、文字列が数値として正しいか、必須項目が入力されているかといった入力バリデーションは別途必要です。プレースホルダーはSQLの値を安全に扱う仕組みであり、入力形式の検証や認証・認可を代替するものではありません。

プレースホルダーを使えるのは値の部分です。テーブル名やカラム名などの識別子、SQLキーワードをプレースホルダーへ置き換えることはできません。動的に識別子を切り替える場合は、受け付ける値をあらかじめ許可リストで管理します。

executeに値を渡す書き方と使い分け

単純な処理では、prepare後のexecuteへ連想配列を渡す方法が簡潔です。配列のキーとSQL中のプレースホルダー名を対応させます。

PHP
<?php
$name = $_GET['name'] ?? '';

$stmt = $pdo->prepare(
    'SELECT id, name FROM users WHERE name = :name'
);
$stmt->execute([
    'name' => $name,
]);

// 期待される挙動::nameへ値を渡してSQLを実行する
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
方法向いているケース注意点
bindValue整数や文字列などの型を明示したい場合バインド後は通常、引数なしでexecuteする
executeへ配列記述量を減らし、単純な入力値を渡したい場合値はPDO::PARAM_STRとして扱われるため、型に注意する
bindValueとexecuteへ配列を渡す方法の使い分け

同じステートメントを異なる値で繰り返し使う場合は、prepareを一度実行し、値を変えてexecuteします。名前付きプレースホルダーと疑問符プレースホルダーは、同じSQL文の中で混在させないようにします。

実務で安全性と保守性を高めるPDOの設定・エラー処理

PDO接続時にPDO::ATTR_ERRMODEをPDO::ERRMODE_EXCEPTIONへ設定すると、PDOのエラーを例外として扱いやすくなります。try-catchで処理し、利用者へはデータベース内部の詳細を表示せず、詳細情報はアクセス制限されたログへ記録する方針が一般的です。

  • 接続情報はソースコードへ固定せず、環境変数や安全な設定管理を検討する。
  • 入力値の必須チェック、型、長さ、形式の検証をSQL実行前に行う。
  • 認証・認可を実装し、SQLインジェクション対策だけでアクセス制御を済ませない。
  • SQLや例外メッセージへパスワードなどの機密情報を出力しない。
  • PHPのバージョンや利用するPDOドライバーによって細かな挙動が異なるため、対象環境の公式ドキュメントを確認する。

最終的なコードは、接続先、PDOドライバー、テーブル構造、入力仕様を実環境に合わせて確認してから利用してください。特にexecuteへ値を渡す方法とbindValueによる型指定は、扱う値の種類やドライバーの仕様を踏まえて選択します。

参考