[PHPのはじめ方] Part15: バリデーションとサニタイズ

はじめに

ユーザーがフォームから送ってくる情報は、そのまま信用してはいけません!😲 悪意のあるユーザーが、予期しないデータや危険なスクリプトを送りつけてくる可能性があります。

そこで重要になるのがバリデーションサニタイズです。これらは、Webアプリケーションを安全に保つための基本的な防御策となります。

このステップでは、フォームから受け取ったデータを安全に扱うための「バリデーション」と「サニタイズ」について学びます。しっかり理解して、安全なコードを書けるようになりましょう!💪

バリデーションとは? 🤔

バリデーション (Validation) とは、ユーザーが入力したデータが、期待する形式やルールに合っているか検証することです。

例えば、

  • メールアドレスの形式になっているか?
  • 電話番号が数字だけで構成されているか?
  • 必須項目が空になっていないか?
  • パスワードが指定した長さ以上か?

などをチェックします。

なぜバリデーションが必要なの?

バリデーションを行わないと、以下のような問題が発生する可能性があります。

  • プログラムのエラー: 予期しないデータ型や値によって、PHPスクリプトがエラーを起こし停止してしまう。
  • データの不整合: データベースに不正なデータが登録されてしまう。
  • セキュリティリスク: 不正なデータを利用した攻撃(後述するXSSやSQLインジェクションなど)の起点となる可能性がある。

バリデーションは、これらの問題を未然に防ぐための重要なステップです。

PHPでのバリデーション方法

PHPには、バリデーションを簡単に行うための関数が用意されています。

1. `filter_var()` 関数

`filter_var()` 関数は、指定したフィルタを使って変数を検証するのに非常に便利です。

<?php
$email = "test@example.com";

// メールアドレス形式か検証
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
    echo "有効なメールアドレスです。";
} else {
    echo "無効なメールアドレスです。";
}

echo "<br>";

$number = "123";

// 整数か検証
if (filter_var($number, FILTER_VALIDATE_INT)) {
    echo "有効な整数です。";
} else {
    echo "無効な整数です。";
}

echo "<br>";

$url = "https://example.com";

// URL形式か検証
if (filter_var($url, FILTER_VALIDATE_URL)) {
    echo "有効なURLです。";
} else {
    echo "無効なURLです。";
}
?>

他にも `FILTER_VALIDATE_BOOLEAN`, `FILTER_VALIDATE_FLOAT` など、様々なフィルタがあります。必要に応じて公式ドキュメント等で確認しましょう。

2. 正規表現 `preg_match()` 関数

より複雑なパターンや独自のルールで検証したい場合は、正規表現を使うことができます。`preg_match()` 関数は、指定したパターンに文字列がマッチするかどうかを調べます。

<?php
$zipcode = "123-4567";

// 郵便番号形式(XXX-XXXX)か検証
if (preg_match("/^\d{3}-\d{4}$/", $zipcode)) {
    echo "有効な郵便番号形式です。";
} else {
    echo "無効な郵便番号形式です。";
}

echo "<br>";

$username = "user_123";

// 半角英数字とアンダースコアのみか検証 (5文字以上15文字以下)
if (preg_match("/^[a-zA-Z0-9_]{5,15}$/", $username)) {
    echo "有効なユーザー名です。";
} else {
    echo "無効なユーザー名です。";
}
?>

正規表現は強力ですが、複雑になりがちなので、まずは `filter_var()` で対応できないか検討しましょう。

サニタイズとは? ✨

サニタイズ (Sanitize) とは、データを無害化することを指します。特にWeb開発においては、HTMLタグやJavaScriptコードなど、ブラウザで特別な意味を持つ可能性のある文字を、単なる文字列として扱えるように変換(エスケープ)することを意味します。

なぜサニタイズが必要なの?

サニタイズの主な目的は、クロスサイトスクリプティング (Cross-Site Scripting, XSS) と呼ばれる攻撃を防ぐことです。

XSS攻撃とは?

攻撃者が、Webサイトの入力フォームなどを通じて悪意のあるスクリプト(JavaScriptなど)を仕込み、他のユーザーがそのページを閲覧した際に、そのスクリプトをユーザーのブラウザ上で実行させる攻撃です。

これにより、

  • セッション情報(ログイン状態など)が盗まれる
  • 偽の入力フォームが表示され、個人情報が盗まれる
  • 他のサイトへ強制的にリダイレクトされる

などの被害が発生する可能性があります。

ユーザーが入力した内容をそのままHTMLに出力してしまうと、もし入力内容に `` のようなJavaScriptコードが含まれていた場合、そのコードがユーザーのブラウザで実行されてしまいます。これを防ぐためにサニタイズが必要です。

PHPでのサニタイズ方法

PHPで最も一般的に使われるサニタイズ関数は `htmlspecialchars()` です。

1. `htmlspecialchars()` 関数

`htmlspecialchars()` 関数は、HTMLにおいて特別な意味を持つ文字(例: `<` や `>`)を、それに対応するHTMLエンティティ(例: `<` や `>`)に変換します。これにより、ブラウザはそれらの文字をHTMLタグとして解釈せず、単なる文字列として表示します。

<?php
// 悪意のある可能性のある入力
$input = "<script>alert('XSS!');</script>";

// htmlspecialchars() でサニタイズ
$sanitized_input = htmlspecialchars($input, ENT_QUOTES, 'UTF-8');

// サニタイズされた結果を出力
echo $sanitized_input;
// 出力結果: &lt;script&gt;alert(&#039;XSS!&#039;);&lt;/script&gt;
// ブラウザ上では <script>alert('XSS!');</script> という文字列として表示される
?>

第二引数 `ENT_QUOTES` は、シングルクォートとダブルクォートの両方を変換するように指定します。第三引数 `’UTF-8’` は、文字エンコーディングを指定します。これらはセキュリティ上、指定することが推奨されます。

ポイント: サニタイズは、主にデータをブラウザに出力する直前に行います。

2. `filter_var()` 関数によるサニタイズ

`filter_var()` 関数にもサニタイズ用のフィルタがあります。例えば、`FILTER_SANITIZE_EMAIL` はメールアドレスから不正な文字を取り除き、`FILTER_SANITIZE_URL` はURLから不正な文字を取り除きます。

<?php
$email = "test(comment)@example.com";
$sanitized_email = filter_var($email, FILTER_SANITIZE_EMAIL);
echo $sanitized_email; // 出力: test@example.com

echo "<br>";

$url = "http://example.com/ path with spaces";
$sanitized_url = filter_var($url, FILTER_SANITIZE_URL);
echo $sanitized_url; // 出力: http://example.com/pathwithspaces
?>

注意: かつてよく使われていた `FILTER_SANITIZE_STRING` は、PHP 8.1で非推奨になりました。これは意図しない動作をすることがあり、セキュリティ上の問題を引き起こす可能性があったためです。文字列のサニタイズには基本的に `htmlspecialchars()` を使用しましょう。

バリデーションとサニタイズの実装例

実際のフォーム処理で、バリデーションとサニタイズをどのように組み合わせるか見てみましょう。ここでは、名前とメールアドレスを受け取る簡単なフォームを例にします。

HTMLフォーム (`form.html`)

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>フォーム</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.0/css/bulma.min.css">
</head>
<body>
  <section class="section">
    <div class="container">
      <h1 class="title">お問い合わせ</h1>
      <form action="process.php" method="post">
        <div class="field">
          <label class="label" for="name">お名前 (必須)</label>
          <div class="control">
            <input class="input" type="text" id="name" name="name" required>
          </div>
        </div>
        <div class="field">
          <label class="label" for="email">メールアドレス (必須)</label>
          <div class="control">
            <input class="input" type="email" id="email" name="email" required>
          </div>
        </div>
        <div class="field">
          <div class="control">
            <button class="button is-link" type="submit">送信</button>
          </div>
        </div>
      </form>
    </div>
  </section>
</body>
</html>

PHP処理 (`process.php`)

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>処理結果</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.0/css/bulma.min.css">
</head>
<body>
  <section class="section">
    <div class="container">
      <h1 class="title">処理結果</h1>
<?php
// エラーメッセージを格納する配列
$errors = [];

// POSTリクエストか確認
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // データの受け取りとトリム(前後の空白削除)
    $name = isset($_POST['name']) ? trim($_POST['name']) : '';
    $email = isset($_POST['email']) ? trim($_POST['email']) : '';

    // --- バリデーション ---
    // 名前のバリデーション (必須チェック)
    if ($name === '') {
        $errors[] = 'お名前は必須入力です。';
    }

    // メールアドレスのバリデーション (必須チェック)
    if ($email === '') {
        $errors[] = 'メールアドレスは必須入力です。';
    } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        // メールアドレス形式チェック
        $errors[] = '有効なメールアドレス形式で入力してください。';
    }

    // --- 処理分岐 ---
    if (empty($errors)) {
        // バリデーションOKの場合
        echo '<div class="notification is-success">';
        echo '送信を受け付けました。<br>';
        // --- サニタイズして表示 ---
        echo 'お名前: ' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '<br>';
        echo 'メールアドレス: ' . htmlspecialchars($email, ENT_QUOTES, 'UTF-8');
        echo '</div>';
        // ここでデータベースへの保存やメール送信などの処理を行う
    } else {
        // バリデーションNGの場合
        echo '<div class="notification is-danger">';
        echo '入力内容にエラーがあります:<br>';
        echo '<ul>';
        foreach ($errors as $error) {
            // エラーメッセージは固定文字列なのでhtmlspecialcharsは不要だが、念のため適用
            echo '<li>' . htmlspecialchars($error, ENT_QUOTES, 'UTF-8') . '</li>';
        }
        echo '</ul>';
        echo '</div>';
        echo '<a href="form.html" class="button is-link">フォームに戻る</a>';
    }

} else {
    // POST以外のリクエストの場合
    echo '<div class="notification is-warning">フォームから送信してください。</div>';
    echo '<a href="form.html" class="button is-link">フォームに戻る</a>';
}
?>
    </div>
  </section>
</body>
</html>

この例では、

  1. まず `trim()` で入力値の前後の空白を削除。
  2. `empty()` や `filter_var()` でバリデーションを実行し、問題があれば `$errors` 配列に追加。
  3. `$errors` 配列が空(エラーなし)の場合のみ、`htmlspecialchars()` でサニタイズしてから画面に表示しています。
  4. エラーがある場合は、エラーメッセージを表示します。

このように、まずバリデーションでデータの正しさを確認し、表示する際にはサニタイズを行うのが基本的な流れです。

まとめ

  • バリデーション: データが期待される形式ルールに合っているか検証すること。(`filter_var`, `preg_match` など)
  • サニタイズ: データを無害化すること。特にHTML/JSとして解釈されるのを防ぐためにエスケープすること。(`htmlspecialchars` など)
  • ユーザーからの入力は信用せず、必ずバリデーションとサニタイズを行う。
  • バリデーションは入力データ受け取り直後に、サニタイズは主にデータ表示直前に行う。
  • これらはXSSなどのセキュリティ攻撃を防ぐために不可欠。

バリデーションとサニタイズは、安全なWebアプリケーションを開発するための基本中の基本です。面倒に感じるかもしれませんが、ユーザーと自分のサービスを守るために、必ず習慣づけましょう!🔒

次のステップでは、ユーザーの状態を維持するための「セッションとクッキーの管理」について学びます。お楽しみに!🚀