導入
Webアプリは、見知らぬ第三者から常に攻撃を受ける前提で作らなければなりません。ここでは、PHPで最も事故が起きやすい4つの脅威と、その対策を横断的にまとめます(コード例の理解が目的なので読み物として進みます)。
説明
flowchart LR
A[ユーザー入力] --> B{信用できる入力か}
B -->|検証・エスケープ・プレースホルダ| C[安全な処理]
B -->|そのまま使ってしまう| D[脆弱性: SQLi / XSS / CSRF]
① SQLインジェクション。 ユーザーの入力をそのまま文字列連結でSQLに組み込むと、入力の中に悪意あるSQLを混ぜ込まれ、データベースを不正に操作されてしまいます。
<?php
// 危険な例:入力をそのまま文字列に埋め込んでいる
$id = $_GET["id"]; // 例えば "1 OR 1=1" のような値が来ると全件漏れる
$sql = "SELECT * FROM users WHERE id = " . $id; // ← 絶対にやってはいけない
// 安全な例:プレースホルダを使う(レッスン24で学んだPDO)
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]); // 値としてしか解釈されないので安全
対策は一つだけ、必ずプレースホルダ(? や :name)を使い、SQL文字列を自分で組み立てないことです。
② XSS(クロスサイトスクリプティング)。 ユーザーの入力をそのままHTMLとして表示すると、入力の中に <script> タグを混ぜ込まれ、他の利用者のブラウザで悪意あるコードが実行されてしまいます。
<?php
// 危険な例
$comment = $_POST["comment"]; // 例えば <script>...</script> が来るかもしれない
echo "<p>{$comment}</p>"; // ← そのまま出すと実行されてしまう
// 安全な例:htmlspecialchars() で無害化してから出す
echo "<p>" . htmlspecialchars($comment) . "</p>";
対策は、ユーザーが入力した値を画面に出すときは必ず htmlspecialchars() を通すことです(レッスン19・23でも触れました。実務で最も忘れやすく、最も事故になりやすい箇所です)。
③ CSRF(クロスサイトリクエストフォージェリ)。 ログイン中の利用者が、悪意あるサイトを開いただけで、本人の意図しないリクエスト(例:パスワード変更、送金)を自分のサイトに送らされてしまう攻撃です。対策は、フォームに推測できないランダムなトークンを埋め込み、送信時にそのトークンが一致するかを確認することです。
<?php
session_start();
// フォーム表示時:トークンを生成してセッションと画面の両方に持たせる
$_SESSION["csrf_token"] = $_SESSION["csrf_token"] ?? bin2hex(random_bytes(16));
$token = $_SESSION["csrf_token"];
echo '<input type="hidden" name="csrf_token" value="' . $token . '">';
// フォーム送信時:セッションのトークンと送られてきたトークンを比較
if (($_POST["csrf_token"] ?? "") !== ($_SESSION["csrf_token"] ?? "")) {
die("不正なリクエストです");
}
外部サイトはこのランダムなトークンを知らないため、正規のフォームを経由しないリクエストは弾かれます。
④ パスワードの保存。 パスワードは絶対に平文(そのままの文字列)でデータベースに保存してはいけません。漏えいすると全ユーザーのパスワードがそのまま盗まれてしまうためです。PHP には安全にハッシュ化・検証する専用関数があります。
<?php
$hash = password_hash("mySecretPass123", PASSWORD_DEFAULT); // 保存するのはこのハッシュ値だけ
echo $hash, "\n"; // 実行のたびに違う文字列になる(ソルトが自動で加わるため)
// ログイン時:入力されたパスワードとハッシュを比較する
var_dump(password_verify("mySecretPass123", $hash)); // true
var_dump(password_verify("wrongPass", $hash)); // false
password_hash() は内部で強力なアルゴリズム(bcrypt など)とランダムな「ソルト」を使ってハッシュ化するため、同じパスワードでも実行のたびに違う文字列になります。password_verify() は入力されたパスワードがそのハッシュに対応するかどうかだけを安全に確認します。自分で md5() などを使って簡易的にハッシュ化するのは危険なので、必ずこの2つの専用関数を使いましょう。
演習
(読み物のため自動判定はありません)上のXSSの「危険な例」と「安全な例」について、入力値が <b>太字</b> だった場合に画面にどう表示されるかを紙に書き出して比較してみましょう。「安全な例」では <b> がそのままタグとして解釈されず、文字として表示されることを確認できれば理解できています。