ノウハウ サイト運営 読了目安 約7分

PHPのお問い合わせフォームで最低限やるべきセキュリティ対策

お問い合わせフォームは、サイトの中でいちばん外部からの入力を受け付ける場所です。設置してしばらくすると、海外からのスパムが毎日届くようになることも珍しくありません。自分のサイトで入れている対策を、理由と一緒に紹介します。

フォームで起きうる4つの問題

お問い合わせフォームは、誰でも自由に文字を送れる入口です。対策をしないと、次のような問題が起こり得ます。

問題何が起きるか主な対策
XSS(クロスサイトスクリプティング)入力されたスクリプトが確認画面などで実行される出力時のエスケープ
メールヘッダーインジェクションフォームが迷惑メールの送信に悪用される改行の除去、入力値をヘッダーに入れない
CSRF(クロスサイトリクエストフォージェリ)他のサイトから勝手に送信させられるトークンの照合
スパム投稿プログラムによる大量の宣伝投稿ハニーポット、時間チェック、reCAPTCHA

このサイトのお問い合わせフォームでも、ここで紹介する対策のうち、出力のエスケープ、改行の除去、トークンによるCSRF対策、メール形式のチェックを入れています。順番に見ていきます。

1. 出力するときは必ずエスケープする

入力された内容を確認画面やエラー表示で画面に出すときは、HTMLとして解釈されないように変換(エスケープ)します。PHPでは htmlspecialchars を使います。毎回書くのは面倒なので、短い関数を用意しておくと漏れが防げます。

function e($value): string
{
    return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
}

// 使うとき
echo '<input type="text" name="name" value="' . e($name) . '">';

ポイントは「入力を受け取ったとき」ではなく「出力するとき」にエスケープすることです。受け取った時点で変換してしまうと、メール本文に &amp; のような文字がそのまま入ってしまいます。HTMLに出すときはHTML用に、メールに書くときはそのまま、と出力先に合わせて処理します。

2. メールヘッダーに改行を入れさせない

メールは、宛先や件名などの「ヘッダー」と「本文」でできていて、ヘッダーは改行で区切られています。もし入力された名前やメールアドレスをヘッダーに入れていて、そこに改行が含まれていると、攻撃者は勝手にヘッダーを追加できます。たとえば Bcc: を追加されると、あなたのサーバーから大量の迷惑メールが送られてしまいます。

// ヘッダーに使う値から改行を取り除く
$safeEmail = str_replace(["\r", "\n"], '', $email);
$safeName  = str_replace(["\r", "\n"], '', $name);

// メールアドレスの形式もチェックする
if (!filter_var($safeEmail, FILTER_VALIDATE_EMAIL)) {
    $errors[] = 'メールアドレスの形式が正しくありません。';
}

さらに大切なのが、送信元(From)に入力されたアドレスを使わないことです。Fromは自分のドメインのアドレスに固定し、相手のアドレスは返信先(Reply-To)に入れます。入力されたアドレスをFromにすると、なりすましメールとして扱われ、迷惑メールフォルダに入ったり届かなかったりします。

$headers = [
    'From: noreply@example.com',          // 自分のドメインに固定
    'Reply-To: ' . $safeEmail,            // 返信先として相手のアドレス
    'Content-Type: text/plain; charset=UTF-8',
];
mb_language('Japanese');
mb_internal_encoding('UTF-8');
mb_send_mail('info@example.com', '【お問い合わせ】' . $safeName . '様', $body, implode("\r\n", $headers));

3. CSRFトークンで「本当にこのフォームからの送信か」を確かめる

CSRFは、別のサイトに仕込んだフォームから、あなたのサイトのフォームに勝手に送信させる攻撃です。お問い合わせフォームでは大きな被害になりにくいものの、会員機能などに広げたときにも必要になる基本の対策なので、最初から入れておきます。

仕組みは、フォームを表示するときにランダムな文字列(トークン)をセッションに保存し、同じ値をフォームの隠し項目にも入れておく、というものです。送信されたときに2つが一致すれば、自分のサイトのフォームから送られたと判断できます。

session_start();

// フォームを表示するとき
if (empty($_SESSION['contact_token'])) {
    $_SESSION['contact_token'] = bin2hex(random_bytes(32));
}
// <input type="hidden" name="token" value="<?= e($_SESSION['contact_token']) ?>">

// 送信を受け取ったとき
$token = (string)($_POST['token'] ?? '');
if (!hash_equals($_SESSION['contact_token'] ?? '', $token)) {
    http_response_code(400);
    exit('不正な送信です。ページを読み込み直してください。');
}
// 送信が完了したらトークンを作り直す
$_SESSION['contact_token'] = bin2hex(random_bytes(32));

比較に == ではなく hash_equals を使うのは、比較にかかる時間の差からトークンを推測される攻撃を防ぐためです。トークンには、推測できない random_bytes を使います。

4. スパム投稿を減らす

フォームを公開してしばらくすると、プログラムによる宣伝の投稿が届き始めます。完全に防ぐのは難しいですが、組み合わせることでかなり減らせます。

ハニーポット(おとりの入力欄)

人間には見えない入力欄を1つ置いておきます。プログラムは見つけた入力欄をすべて埋める傾向があるため、ここに何か入っていれば機械による送信と判断できます。

<!-- フォーム内(CSSで画面外に移動して見えなくする) -->
<div class="hp-field" aria-hidden="true">
  <label>空欄のままにしてください<input type="text" name="website" tabindex="-1" autocomplete="off"></label>
</div>
// 受け取り側
if (($_POST['website'] ?? '') !== '') {
    // 機械による送信とみなし、エラーを出さずに完了画面を見せる
    header('Location: /contact?sent=1');
    exit;
}

display: none で隠すと見抜くプログラムもあるため、position: absolute; left: -9999px; のように画面の外に出す方法がよく使われます。

送信までの時間をチェック

フォームを表示した時刻をセッションに保存し、送信までが数秒以内なら機械とみなす方法です。人間が名前と本文を入力するには、少なくとも数秒はかかります。

reCAPTCHAなどの確認

それでも多い場合は、GoogleのreCAPTCHAのような仕組みを導入します。このサイトのフォームも、設定ファイルにキーを入れればreCAPTCHAが有効になる作りにしています。ただし、外部のサービスを読み込むことになるため、プライバシーポリシーへの記載も必要です。

文字数とURLの数を制限する

本文の文字数に上限を設けたり、本文に含まれるURLが多すぎる投稿を弾いたりするのも有効です。宣伝目的の投稿は、ほとんどがURLを含んでいます。

見落としがちなこと

  • HTTPSで送る:フォームのページと送信先は、必ずHTTPSにします。HTTPのままだと、入力内容が途中で読まれる可能性があります。
  • エラーメッセージで内部を見せない:送信に失敗したときに、PHPのエラーやサーバーのパスを画面に出さないようにします。本番環境では display_errors をオフにします。
  • 二重送信を防ぐ:送信後は完了ページへリダイレクトする(PRGパターン)と、ブラウザの再読み込みで同じ内容が再送されるのを防げます。
  • 届くかテストする:迷惑メールフォルダに入っていないか、自分で何度か送って確認します。サーバーのSPFやDKIMの設定も、届きやすさに影響します。

まとめ

  1. 画面に出すときは htmlspecialchars でエスケープする。
  2. ヘッダーに使う値から改行を取り除き、Fromは自分のアドレスに固定する。
  3. トークンと hash_equals でCSRFを防ぐ。
  4. ハニーポットや時間チェックでスパムを減らす。
  5. HTTPS、エラー表示、二重送信にも気を配る。

フォームは一度作ると見直す機会が少ない部分です。年に一度くらいは、迷惑メールの届き具合やPHPのバージョンとあわせて点検しておくと安心です。