はじめに (対象読者・この記事でわかること)
このブログは、Webアプリケーション開発に携わるエンジニアや、PHPでのデータ処理を学び始めた初心者・中級者を対象としています。
具体的には、HTTP GET リクエストで送られてくるパラメータを、そのままデータベースへ渡すと起こり得る SQLインジェクション の危険性を理解し、PDO のプリペアドステートメント を用いた安全な実装方法が身につきます。
記事を書くきっかけは、社内プロジェクトで頻繁に見られた「GET の値を直接クエリ文字列に埋め込んでしまう」コードを改善した経験です。実例を交えて、正しい書き方と落とし穴の回避策をまとめました。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- HTML の基本的な構造とフォーム送信の仕組み
- PHP の基本文法(変数、配列、関数)と PDO の概要
GETパラメータとSQLインジェクションのリスク
Web アプリでユーザーからの入力をそのまま SQL 文に組み込むと、SQLインジェクション と呼ばれる攻撃に晒されます。たとえば、以下のようなコードは危険です。
Php$id = $_GET['id']; $sql = "SELECT * FROM users WHERE id = $id";
ユーザーが id=1 OR 1=1 のような文字列を渡すと、意図しない全件取得やデータ改ざんが可能になります。
この問題を防ぐには、入力値のバリデーション と SQL 文のプレースホルダ化 が必須です。プレースホルダ化は、データベース側で値とクエリ構造を分離して扱うことで、文字列がクエリとして解釈されることを防ぎます。PHP では PDO(PHP Data Objects)を利用するのが標準的かつ推奨される手法です。
GETパラメータを安全にSQLへ渡す実装手順
以下では、実際に GET パラメータ id を受け取り、users テーブルから該当レコードを取得するまでの一連の流れを解説します。ポイントは 「取得 → バリデーション → プレースホルダ化 → 実行」 の順番です。
ステップ1:データ取得とバリデーション
まずは $_GET から値を取得し、想定する型・範囲をチェックします。数値であることを確認し、整数にキャストするだけでも多くの不正入力を排除できます。
Php// GET パラメータ取得 $id = $_GET['id'] ?? null; // バリデーション:数値かつ正の整数かを確認 if ($id === null || !ctype_digit($id) || (int)$id <= 0) { http_response_code(400); exit('Invalid ID parameter.'); } // 安全に整数へ変換 $id = (int)$id;
ここで ctype_digit を使うと、文字列であっても「全て数字か」を簡単に判定でき、(int)$id で整数に変換します。これだけで SQL インジェクションの大部分は防げますが、さらに安全性を高めるために次のステップでプレースホルダを使用します。
ステップ2:PDO 接続設定
データベース接続は例外をスローするように設定し、エラーモードを ERRMODE_EXCEPTION にします。これにより、接続エラーやクエリ失敗時に例外が投げられ、プログラムが適切に対処できます。
Php$dsn = 'mysql:host=localhost;dbname=myapp;charset=utf8mb4'; $user = 'dbuser'; $pass = 'dbpass'; try { $pdo = new PDO($dsn, $user, $pass, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // エミュレートプリペアドステートメントを無効化(実際のプリペアドステートメントを使用) PDO::ATTR_EMULATE_PREPARES => false, ]); } catch (PDOException $e) { // 本番環境ではエラーログに出力し、ユーザーには汎用メッセージだけを返す error_log('DB Connection error: ' . $e->getMessage()); http_response_code(500); exit('Database connection failed.'); }
ATTR_EMULATE_PREPARES を false にすると、MySQL のネイティブなプリペアドステートメントが使用され、バインドされた値は文字列操作ではなくバイナリレベルで処理されるため、SQLインジェクション防止効果が高まります。
ステップ3:プリペアドステートメントの作成とバインド
安全なクエリはプレースホルダ :id を使って記述し、bindValue で整数型としてバインドします。
Php$sql = 'SELECT id, name, email FROM users WHERE id = :id'; $stmt = $pdo->prepare($sql); $stmt->bindValue(':id', $id, PDO::PARAM_INT); $stmt->execute(); $user = $stmt->fetch(); if ($user) { // JSON で結果を返す例 header('Content-Type: application/json'); echo json_encode($user, JSON_UNESCAPED_UNICODE); } else { http_response_code(404); exit('User not found.'); }
bindValue の第3引数に PDO::PARAM_INT を明示すると、PHP が自動的に整数として扱うことを保証します。これにより、たとえ文字列が混入しようとしてもデータベース側で型が強制され、インジェクションが防がれます。
ハマった点やエラー解決
- エラー:
SQLSTATE[HY093]: Invalid parameter number - 原因はプレースホルダ名と
bindValueのキーが一致していないケースです。コロン:を付け忘れた、あるいは名前が違うと発生します。 -
解決策は、
prepare時のクエリとbindValueのキーを正確に合わせることです。 -
エラー:
PDOExceptionがキャッチされない - 接続時に
ATTR_ERRMODEをERRMODE_EXCEPTIONに設定していないと、エラーは警告として出力され、例外が投げられません。 -
解決策は、
new PDO(..., [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ])を必ず指定することです。 -
UTF-8 文字化け
- MySQL の文字コードが
utf8mb4でない場合、取得した日本語が文字化けします。 - 解決策は、DSN に
charset=utf8mb4を付与し、テーブル・カラムも同じ文字セットに統一することです。
解決策まとめ
- バリデーションで型を限定 →
ctype_digit+整数キャスト - PDO のエラーモードを例外に設定 →
ERRMODE_EXCEPTION - エミュレート無効 →
ATTR_EMULATE_PREPARES => false - プレースホルダに正しい型でバインド →
PDO::PARAM_INT - エラーログに記録し、ユーザーには汎用メッセージ → セキュリティ向上
これらを順守すれば、GET パラメータを安全にデータベースへ渡す基礎が固まります。
まとめ
本記事では、PHP で受け取った GET パラメータを SQLインジェクションから守りながら データベースへ渡す手順を、一から実装例と共に解説しました。
- バリデーションで入力を制限し、PDO のプリペアドステートメントでクエリを安全に組み立てる。
- エラーハンドリングと 文字コード設定 も忘れずに行うことで、実運用でも安心して利用できる。
この記事を通じて、読者は「安全なデータ取得の基本パターン」を身につけ、実務でのコード品質向上に役立てられるはずです。次回は、POST データや JSON リクエストを同様に安全に扱う方法を取り上げる予定です。
参考資料
- PHP: PDO - Manual
- SQLインジェクションとは – OWASP
- 書籍『PHPフレーズブック 改訂版』技術評論社(2022)
