PDOを使う前に知っておきたいSQLインジェクションの危険性
検索語やログインIDなどの入力値をSQL文字列へ直接連結すると、入力内容がSQLの構文として解釈される可能性があります。たとえば、検証環境で次のような実装を使うと、入力値に条件を追加する文字列が含まれた場合に、意図しない検索結果になるおそれがあります。
<?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を実行します。
flowchart LR
A[PDO接続] --> B[prepareでSQLを準備]
B --> C[bindValueで値と型を指定]
C --> D[executeで実行]図を表示できませんでした。Mermaidコードを確認してください。
bindValueを使わず、executeへ値の配列を渡す書き方もあります。PDO公式マニュアルでは、executeへ渡すパラメータは入力専用の値として扱われ、デフォルトではPDO::PARAM_STRとして扱われると説明されています。型を明示したい場合はbindValueが適しています。
prepareとbindValueで安全にSQLを実行する手順
次の例は、接続先、データベース名、テーブル構造を実環境に合わせて置き換える汎用例です。入力値をSQLへ連結せず、名前付きプレースホルダーへバインドしています。
<?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
$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として扱われるため、型に注意する |
同じステートメントを異なる値で繰り返し使う場合は、prepareを一度実行し、値を変えてexecuteします。名前付きプレースホルダーと疑問符プレースホルダーは、同じSQL文の中で混在させないようにします。
実務で安全性と保守性を高めるPDOの設定・エラー処理
PDO接続時にPDO::ATTR_ERRMODEをPDO::ERRMODE_EXCEPTIONへ設定すると、PDOのエラーを例外として扱いやすくなります。try-catchで処理し、利用者へはデータベース内部の詳細を表示せず、詳細情報はアクセス制限されたログへ記録する方針が一般的です。
- 接続情報はソースコードへ固定せず、環境変数や安全な設定管理を検討する。
- 入力値の必須チェック、型、長さ、形式の検証をSQL実行前に行う。
- 認証・認可を実装し、SQLインジェクション対策だけでアクセス制御を済ませない。
- SQLや例外メッセージへパスワードなどの機密情報を出力しない。
- PHPのバージョンや利用するPDOドライバーによって細かな挙動が異なるため、対象環境の公式ドキュメントを確認する。
最終的なコードは、接続先、PDOドライバー、テーブル構造、入力仕様を実環境に合わせて確認してから利用してください。特にexecuteへ値を渡す方法とbindValueによる型指定は、扱う値の種類やドライバーの仕様を踏まえて選択します。