PHPのお問い合わせフォームに、CSRF対策を実装する
こんにちは、UPLIFT代表のしかたこうきです。前回は、HTMLとPHPによる、必要最低限のシンプルなお問い合わせフォームを作ってみました。
ただ、前回の記事にもある通り、一切のセキュリティ対策を施していません。
この記事では、お問い合わせフォームに必須のセキュリティ対策の1つである、CSRF対策の実装をしていきます。
前回の続きになりますので、必要な方はGitHubの方からソースコードをダウンロードしてください。
CSRFとは?
クロスサイトリクエストフォージェリの略です。詳しくはググってもらったらいくらでも解説が出てくるので、そちらをご参照ください。例えば、この記事なんか、とてもわかりやすいと思います。
とにかくCSRFは、フォームについてまわるセキュリティリスクのことです。お問い合わせフォームの場合は、お問い合わせフォームを通じた大量のスパムを送りつけられるなどの被害リスクが想定されます。
もちろん、そんなことになっては大変ですので、対策を行います。
CSRF対策は具体的にどうすればいい?
お問い合わせフォームのCSRF対策は、ワンタイムトークンという、一種の合言葉のようなものを利用します。
あらかじめ、サーバー側から合言葉をHTMLのフォーム画面に送っておきます。これはinput type="hidden"という、特殊なフォームフィールドに収めておきます(画面からは直接見えない)
次に、このフォームから送信された合言葉が、実際にサーバーから送った合言葉と一致しているかどうかをチェックします。
この合言葉は、アクセスするたびに内容が変わる仕組みになっています(ワンタイムトークン)なので、一度フォームにアクセスしてソースコードの中に記載されている合言葉を調べても、次のタイミングに別の画面からアクセスした時には合言葉は変わってしまっているため、外部から意図しないPOSTができないようになります。
最初は「何のこっちゃ」という感じかもしれませんが、とりあえずは「こういうものだ」と思って、お手本通りに実装してもらえれば大丈夫です。
実際に実装してみる
ワンタイムトークンを発行するには、便利なComposerライブラリなどもあるのですが、勉強のためにも、今回は手作業で実装します。
前回はcontact.htmlとcontact.phpという2つのファイルでフォームを作成しました。
今回は、contact.htmlを、index.phpに名前を変更します。これは、ワンタイムトークンの生成にはHTMLではなく、PHPが必要となるからです。
ワンタイムトークンの生成
次に、index.phpの冒頭にPHPを追記します。
<?php
// セッションの利用を開始
session_start();
// ワンタイムトークン生成
$toke_byte = openssl_random_pseudo_bytes(16);
$csrf_token = bin2hex($toke_byte);
// トークンをセッションに保存
$_SESSION['csrf_token'] = $csrf_token;
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>お問い合わせフォーム</title>
</head>
<body>
<form action="contact.php" method="POST">
<div>
<label for="name">お名前:</label>
<input type="text" id="name" name="name" />
</div>
<!-- 以下、続く -->
セッションというのは、サーバー側でアクセスしたユーザーの情報を保管しておく機能のことです。セッションを使うことによって、フォーム入力画面とPOST後のPHPで同じワンタイムトークンの内容を保持することができます(逆に言えば、セッションを使わないと、このようなことができない)
openssl_random_pseudo_bytes()やbin2hex()は、合言葉となるワンタイムトークンを生成するための関数です。本来はトークン生成のためのものではないのですが、ランダムな文字列を簡単に生成できるので、この2つがよく用いられます。
最後に、$_SESSION['csrf_token']という、セッションにデータ保存する変数を利用して、トークンを保存、次の画面(PHP)に持ち越せるようにします。
トークンをhiddenフィールドにセット
次に、先ほど生成したトークンを、HTMLのinput type="hidden"フィールドに追加します。たいてい、フォームの最後の方に追加することが多いですが、実際には画面に見えないので、formタグの中であればどこでも良いです。name属性もしっかり指定しておきましょう。
<body>
<form action="contact.php" method="POST">
<div>
<label for="name">お名前:</label>
<input type="text" id="name" name="name" />
</div>
<div>
<label for="email">メールアドレス:</label>
<input type="email" id="email" name="email" />
</div>
<div>
<label for="message">お問い合わせ本文</label>
<textarea id="message" name="message"></textarea>
</div>
<input type="hidden" name="csrf_token" value="<?php echo $csrf_token; ?>" />
<!-- ↑追加 -->
<button type="submit">送信</button>
</form>
</body>
contact.php側の実装
続いて、メール送信担当部分である、contact.php側の実装も行います。
冒頭に、以下のように記載します。
session_start();
// ワンタイムトークンの一致を確認
if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
// トークンが一致しなかった場合
die('お問い合わせの送信に失敗しました');;
}
//以下からは前回の続き
mb_language("Japanese");
//↑マルチバイトの言語設定を日本語にします
mb_internal_encoding("UTF-8");
//↑マルチバイトの文字エンコーディングをUTF-8にします
// 以降続きます
1行目が初見の人には難しいと思いますが、要は
name属性「csrf_token」の中身が無い、または
name属性「csrf_token」とセッション「csrf_token」が一致しない場合
という意味になります。||が「または」という意味で、「!==」が一致しない、という意味ですね。
つまり、フォームのPOSTで送られてきたトークンと、セッションに保存されたトークンが一致せず、そもそもトークンがない場合には一致すらできない、というわけで、どちらも合言葉不一致でCSRF攻撃が疑われるPOSTとなります。
この場合はdie()という関数が実行され、「お問い合わせの送信に失敗しました」というメッセージと共に、PHPの実行を強制終了させます(その先のメール送信は行われない)
実際にメール送信をしてみる
では、実際にメール送信をしてみてください。念の為、index.phpとcontact.phpの全ソースコードを貼っておきます。
index.php
<?php
// セッションの利用を開始
session_start();
// ワンタイムトークン生成
$toke_byte = openssl_random_pseudo_bytes(16);
$csrf_token = bin2hex($toke_byte);
// トークンをセッションに保存
$_SESSION['csrf_token'] = $csrf_token;
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>お問い合わせフォーム</title>
</head>
<body>
<form action="contact.php" method="POST">
<div>
<label for="name">お名前:</label>
<input type="text" id="name" name="name" />
</div>
<div>
<label for="email">メールアドレス:</label>
<input type="email" id="email" name="email" />
</div>
<div>
<label for="message">お問い合わせ本文</label>
<textarea id="message" name="message"></textarea>
</div>
<input type="hidden" name="csrf_token" value="<?php echo $csrf_token; ?>" />
<button type="submit">送信</button>
</form>
</body>
</html>
contact.php
<?php
session_start();
// ワンタイムトークンの一致を確認
if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
// トークンが一致しなかった場合
die('お問い合わせの送信に失敗しました');
}
mb_language("Japanese");
//↑マルチバイトの言語設定を日本語にします
mb_internal_encoding("UTF-8");
//↑マルチバイトの文字エンコーディングをUTF-8にします
if($_POST) {
$to = 'youraddress@example.com';
//↑このお問い合わせフォームに入力された内容を送る先のメールアドレス。
//通常は、お問い合わせフォームがあるホームページを管理している人のメールアドレスです。
$subject = 'お問い合わせがありました';
//↑送信されるメールの題名です。
//↓以下は、送信するメールの本文です。1行ずつ$messageに追記する形です。
// \nは、改行の意味。
$message = "お問い合わせがありました。\n";
$message .= "\n";
$message .= "入力された内容は以下の通りです。\n";
$message .= "---\n";
$message .= "\n";
$message .= "お名前:\n";
$message .= $_POST["name"]; // name属性がnameの内容が入ります
$message .= "\n";
$message .= "メールアドレス:\n";
$message .= $_POST["email"]; // name属性がemailの内容が入ります
$message .= "\n";
$message .= "お問い合わせ本文:\n";
$message .= $_POST["message"]; // name属性がmessageの内容が入ります
//↓最後に、設定した内容でメールを送る命令です
if(mb_send_mail($to,$subject,$message)) {
echo "メールが送信されました";
} else {
echo "メールの送信に失敗しました";
}
} else {
echo "HTMLからのPOST送信受信に失敗しました";
}
これで送信してみると、前回と同じく、特に問題なくメールが送信できたと思います。
これでは、正常時のチェックしかできず、CSRF攻撃を想定した異常時のチェックができません。そこでまず、意図的にワンタイムトークンが存在しない場合の送信を行ってみます。
index.phpにアクセスし、デベロッパーツールを開きます。Elementsタブの中からinput type="hidden" name="csrf_token"を探し、選択します。
選択したら、Deleteキーを押して、トークンのhiddenフィールドを削除します。
その後、送信ボタンを押してみてください。
ご覧のように、しっかり失敗しています。
また、CSRF攻撃というのは、外部から攻撃されるものであるため、外部からのPOSTも想定してテストします。
外部からのPOSTは、Postmanというアプリを使います。Postmanの使い方はこちらの記事などを参考にしてください。
以下が実際にPostmanで外部からPOSTしてみた結果です。
これで、外部からのPOSTもしっかりエラーになることが確認出来ましたね。
ソースコードダウンロード
ここまでのソースコードは、GitHubからダウンロードできます。バージョン0.2.0が、CSRFトークンを追加したところまでのソースコードです(MIT License)
最後に
いかがでしたか。お問い合わせフォームのセキュリティ対策は、これが全てではありませんが、とりあえず重要なCSRF対策については以上となります。思ったよりも難しくなかったのではないでしょうか。
とにかく、hiddenフォームとセッションに収められたワンタイムトークンが一致していればOK、そうでなければエラー、というように作っておけばまず安心ということですね。
POSTを用いるフォームは、必ずこのCSRF対策を施すようにしましょう。
次の記事はこちら
頂いたサポートはクリエイター活動の主に機材費・出張費に充てます! より良い作品アウトプットのためにご協力よろしくお願いします!