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) . '">';
ポイントは「入力を受け取ったとき」ではなく「出力するとき」にエスケープすることです。受け取った時点で変換してしまうと、メール本文に & のような文字がそのまま入ってしまいます。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の設定も、届きやすさに影響します。
まとめ
- 画面に出すときは
htmlspecialcharsでエスケープする。 - ヘッダーに使う値から改行を取り除き、Fromは自分のアドレスに固定する。
- トークンと
hash_equalsでCSRFを防ぐ。 - ハニーポットや時間チェックでスパムを減らす。
- HTTPS、エラー表示、二重送信にも気を配る。
フォームは一度作ると見直す機会が少ない部分です。年に一度くらいは、迷惑メールの届き具合やPHPのバージョンとあわせて点検しておくと安心です。