【コピぺ可】セキュリティに配慮したメールフォームの作り方【PHP + HTML5 】
セキュリティに配慮したメールフォーム・お問い合わせフォームの作り方をご紹介します。バリデーション機能、レスポンシブ、郵便番号からの住所自動入力にも対応しています。一部修正していただければほとんどコピペでいけると思います。ソースの解説もございます。ご利用は自己責任で。
- メールフォームを設置したい方
- メールフォームのセキュリティの脅威について知りたい方
作成するメールフォームの概要
下記のようなメールフォームを作ります。
- レスポンシブデザイン対応
- Bootstrap4 + HTML5のバリデーション機能に対応
- 郵便番号から住所を自動入力
- 自動返信は無し
- CSRF(クロスサイトリクエストフォージェリ / シーサフ)対策
- クリックジャッキング対策
- メールヘッダ・インジェクション対策
レスポンシブデザイン対応
Bootstrap 4を用いてレスポンシブデザインに対応します。
Bootstrap 4 + HTML5のバリデーション機能
Bootstrap 4のFormsを用いてバリデーションを作成します。
Bootstrap 3などを使用しているサイトでは正常に動作しない場合がありますが、その際は別ページで作成してiframeで読み込めば問題無く使用できるかと思います。
Forms – Bootstrap 4.1 日本語リファレンス
郵便番号から住所を自動入力
「YubinBango」を使用して郵便番号入力時に住所を自動で挿入します。
GitHub – yubinbango/yubinbango
自動返信は無し
セキュリティを考慮して自動返信機能は設けません。
お問合せフォームを悪用する攻撃増加に関する注意喚起 | さくらインターネット
【セキュリティ ニュース】サイトの「問い合わせフォーム」を悪用する攻撃に警戒を):Security NEXT
CSRF(クロスサイトリクエストフォージェリ / シーサフ)対策
トークンを用いて他サイトからのリクエストを処理しないよう設定します。
CSRF(クロスサイトリクエストフォージェリ / シーサフ)とは?
クロスサイトリクエストフォージェリ(CSRF)とは、Webアプリケーションに存在する脆弱性、もしくはその脆弱性を利用した攻撃方法のことです。掲示板や問い合わせフォームなどを処理するWebアプリケーションが、本来拒否すべき他サイトからのリクエストを受信し処理してしまいます。
クリックジャッキング対策
他のドメインのサイトからのframe要素やiframe要素による読み込みを制限します。
クリックジャッキングとは
クリックジャッキングとは、Webサイト上に隠蔽・偽装したリンクやボタンを設置し、サイト訪問者を視覚的に騙してクリックさせるなど意図しない操作をするよう誘導させる手法です。
メールヘッダ・インジェクション対策
メールヘッダに関する部分で外部入力が必要な項目(今回の場合はReply-to)の入力内容から改行コードを削除することで、メールヘッダインジェクション対策を行います。
メールヘッダインジェクションとは
ウェブアプリケーションの中には、利用者が入力した商品申し込みやアンケート等の内容を、特定のメールアドレスに送信する機能を持つものがあります。一般に、このメールアドレスは固定で、ウェブアプリケーションの管理者以外の人は変更できませんが、実装によっては、外部の利用者がこのメールアドレスを自由に指定できてしまう場合があります。このような問題を引き起こす脆弱性を「メールヘッダ・インジェクション」と呼び、それを悪用した攻撃を、「メールヘッダ・インジェクション攻撃」と呼びます。
安全なウェブサイトの作り方 – 1.8 メールヘッダ・インジェクション:IPA 独立行政法人 情報処理推進機構 より引用
メールフォームのデモ
下記のようなフォームが作成されます。デモなので実際に送信はされませんので適当に触ってください。
実際のソース
下記がソースです。オレンジ太字部分は任意で変更してください。
form.php
<?php session_start(); //クリックジャッキング対策 header( 'X-FRAME-OPTIONS: SAMEORIGIN' ); ?> <!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"> <title>Mail form</title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> </head> <body> <div class="container mt-3"> <!--Mail Form--> <?php //HTML特殊文字エスケープ関数 function h( $str ) { return htmlspecialchars( $str, ENT_QUOTES, 'UTF-8' ); } //メールヘッダインジェクション対策のための改行削除関数 function i( $str ) { return str_replace( array( "\r", "\n" ), '', $str ); } //POSTされたデータを各変数に入れる $your_name = isset( $_POST[ 'your_name' ] ) ? $_POST[ 'your_name' ] : NULL; $your_email = isset( $_POST[ 'your_email' ] ) ? $_POST[ 'your_email' ] : NULL; $your_zip = isset( $_POST[ 'your_zip' ] ) ? $_POST[ 'your_zip' ] : NULL; $your_addr = isset( $_POST[ 'your_addr' ] ) ? $_POST[ 'your_addr' ] : NULL; $your_inquiry = isset( $_POST[ 'your_inquiry' ] ) ? $_POST[ 'your_inquiry' ] : NULL; //関数適用 $your_name = h( $your_name ); $your_email = h( $your_email ); $your_email = i( $your_email ); $your_zip = h( $your_zip ); $your_addr = h( $your_addr ); $your_inquiry = h( $your_inquiry ); //送信プログラム if ( $_SERVER[ 'REQUEST_METHOD' ] != 'POST' ) { //送信ボタンを押して無い場合 //CSRF(クロスサイトリクエストフォージェリ シーサフ)対策としてユニークな文字列トークンをセッションに格納 $_SESSION[ 'token' ] = uniqid( '', true ); } else { //送信ボタンを押した場合 $token = $_POST[ 'token' ]; //送信されたトークンを変数に格納 if ( !( hash_equals( $token, $_SESSION[ 'token' ] ) && !empty( $token ) ) ) { //送信されたトークンがセッションと同じ値か比較して異なる場合 $sendcheck = "failed"; } else { //トークンが一致した場合 // メール設定 mb_language( "ja" ); mb_internal_encoding( "UTF-8" ); $to = "mail@example.com"; //メール送信先、複数の場合はカンマ区切り $subject = 'お問合せフォームよりメールが送信されました'; //メール件名 $from_mail = "noreply@example.com"; // 送信元メールアドレス(迷惑メールと判別されないよう設置サイトと同じドメインのメールアドレスにしてください $from_name = mb_encode_mimeheader( "ほげほげ" ); // 送信者名 $reply_to = $your_email; //返信先 $cc = ''; //CC送信先があれば入力 $bcc = ''; //BCC送信先があれば入力 // メール本文設定 $message = "お問い合わせフォームより、下記内容でメッセージが送信されました。" . "\r\n\r\n"; $message .= "名前:" . $your_name . " \r\n"; $message .= "メールアドレス:" . $your_email . " \r\n"; $message .= "住所:〒" . $your_zip . " " . $your_addr . "\r\n\r\n"; $message .= "お問い合わせ内容:\r\n" . $your_inquiry . " \r\n"; // メールヘッダ設定 $header = "Content-Type: text/plain \r\n"; $header .= "Return-Path: " . $from_mail . " \r\n"; $header .= "From: " . $from_name . "<" . $from_mail . ">\r\n"; $header .= "Sender: " . $from_mail . " \r\n"; $header .= "Reply-To: " . $reply_to . " \r\n"; if ( !empty( $cc ) ) { $header .= "Cc: " . $cc . " \r\n"; } if ( !empty( $bcc ) ) { $header .= "Bcc: " . $bcc . " \r\n"; } $header .= "Organization: " . $from_name . " \r\n"; $header .= "X-Sender: " . $from_mail . " \r\n"; $header .= "X-Priority: 3 \r\n"; mb_send_mail( $to, $subject, $message, $header ); $sendcheck = "send"; } } ?> <?php if($sendcheck == 'send'){ ?> <p class="d-block border border-success p-2">メッセージが送信されました。</p> <p class="mx-2"> 内容確認のうえ、改めてこちらからご連絡差しあげますので、今しばらくお待ちください。</p> <?php }elseif($sendcheck == 'failed'){ ?> <p class="d-block border border-danger p-2">メッセージが送信できませんでした。</p> <p class="mx-2"> フォームを複数タブで開いた場合、エラーが発生する可能性があります。 お手数ですがページを更新した上で再度お試しください。 </p> <?php }else{ ?> <form class="needs-validation h-adr" method="post"> <div class="mb-3"> <label>名前<span class="text-danger ml-1">※</span></label> <input type="text" class="form-control" name="your_name" placeholder="名前" required> <div class="invalid-feedback"> 入力してください </div> </div> <div class="mb-3"> <label>Eメール<span class="text-danger ml-1">※</span></label> <input type="email" class="form-control" name="your_email" placeholder="Eメール" required> <div class="invalid-feedback">入力してください</div> </div> <div class="mb-3 row"> <div class="col-md-3"> <span class="p-country-name" style="display:none;">Japan</span> <label>郵便番号</label> <input type="text" name="your_zip" maxlength="8" class="form-control mb-2 p-postal-code" placeholder="郵便番号"> </div> <div class="col-md-9"> <label>住所</label> <input type="text" name="your_addr" class="form-control p-region p-locality p-street-address p-extended-address" placeholder="住所"> </div> </div> <div class="mb-3"> <label>お問い合わせ内容<span class="text-danger ml-1">※</span></label> <textarea class="form-control" name="your_inquiry" rows="5" placeholder="お問い合わせ内容" required></textarea> <div class="invalid-feedback">入力してください</div> </div> <div class="mb-3"> <div class="custom-control"> <input class="custom-control-input" type="checkbox" value="" id="invalidCheck" required> <label class="custom-control-label" for="invalidCheck">個人情報保護方針に同意する</label> <div class="invalid-feedback mb-3">送信する前に同意する必要があります</div> </div> </div> <input type="hidden" name="token" value="<?php echo $_SESSION['token'] ?>"> <div> <button type="submit" class="btn btn-primary mb-3">送信</button> </div> </form> <?php } ?> <!--//Mail Form--> </div> <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script> <script src="https://yubinbango.github.io/yubinbango/yubinbango.js" charset="UTF-8"></script> <script> (function() { 'use strict'; window.addEventListener('load', function() { var forms = document.getElementsByClassName('needs-validation'); var validation = Array.prototype.filter.call(forms, function(form) { form.addEventListener('submit', function(event) { if (form.checkValidity() === false) { event.preventDefault(); event.stopPropagation(); } form.classList.add('was-validated'); }, false); }); }, false); })(); </script> </body> </html>
ソースの解説
順に解説して参ります。
1〜5行目 セッションの開始、クリックジャッキング対策
header( 'X-FRAME-OPTIONS: SAMEORIGIN' );
を入れることで他ドメインのサイトからのframe要素やiframe要素による読み込みを制限します。
<?php
session_start();
//クリックジャッキング対策
header( 'X-FRAME-OPTIONS: SAMEORIGIN' );
?>
6〜16行目 bootstrap.css の読み込み
bootsrap.css を読み込みます。
<!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"> <title>Mail form</title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> </head> <body> <div class="container mt-3">
17〜20行目 HTML特殊文字エスケープ関数
HTML特殊文字のエスケープのための関数を作成します。
//HTML特殊文字エスケープ関数
function h( $str ) {
return htmlspecialchars( $str, ENT_QUOTES, 'UTF-8' );
}
21〜24行目 メールヘッダインジェクション対策のための改行削除関数
改行削除の関数を作成します。
//メールヘッダインジェクション対策のための改行削除関数
function i( $str ) {
return str_replace( array( "\r", "\n" ), '', $str );
}
25〜37行目 POST されたデータの格納と関数の適用
POST されたデータを変数に格納し、HTML特殊文字エスケープ関数と改行削除の関数を適用します。
送信内容の項目を増やしたい場合にもここに追加で記述する必要があります。
//POSTされたデータを各変数に入れる $your_name = isset( $_POST[ 'your_name' ] ) ? $_POST[ 'your_name' ] : NULL; $your_email = isset( $_POST[ 'your_email' ] ) ? $_POST[ 'your_email' ] : NULL; $your_zip = isset( $_POST[ 'your_zip' ] ) ? $_POST[ 'your_zip' ] : NULL; $your_addr = isset( $_POST[ 'your_addr' ] ) ? $_POST[ 'your_addr' ] : NULL; $your_inquiry = isset( $_POST[ 'your_inquiry' ] ) ? $_POST[ 'your_inquiry' ] : NULL; //関数適用 $your_name = h( $your_name ); $your_email = h( $your_email ); $your_email = i( $your_email ); $your_zip = h( $your_zip ); $your_addr = h( $your_addr ); $your_inquiry = h( $your_inquiry );
38〜46行目 クロスサイトリクエストフォージェリ対策
uniqid( '', true )
でユニークなランダム文字列を生成してトークンとしてセッションに格納します。
送信ボタンを押した際にPOSTされたトークンとセッションに格納されたトークンが同じ値か比較します。
//送信プログラム if ( $_SERVER[ 'REQUEST_METHOD' ] != 'POST' ) { //送信ボタンを押して無い場合 //CSRF(クロスサイトリクエストフォージェリ / シーサフ)対策としてユニークな文字列トークンをセッションに格納 $_SESSION[ 'token' ] = uniqid( '', true ); } else { //送信ボタンを押した場合 $token = $_POST[ 'token' ]; //送信されたトークンを変数に格納 if ( !( hash_equals( $token, $_SESSION[ 'token' ] ) && !empty( $token ) ) ) { //送信されたトークンがセッションと同じ値か比較して異なる場合 $sendcheck = "failed"; } else { //トークンが一致した場合
47〜56行目 送信メール設定
主に編集するのはこの箇所になります。
メールの宛先、件名、From、送信者名、CC、BCCを任意で入力します。Reply-to には送信者が入力したメールアドレスが挿入されます。
// メール設定 mb_language( "ja" ); mb_internal_encoding( "UTF-8" ); $to = "mail@example.com"; //メール送信先、複数の場合はカンマ区切り $subject = 'お問合せフォームよりメールが送信されました'; //メール件名 $from_mail = "noreply@example.com"; // 送信元メールアドレス(迷惑メールと判別されないよう設置サイトと同じドメインのメールアドレスにしてください $from_name = mb_encode_mimeheader( "ほげほげ" ); // 送信者名 $reply_to = $your_email; //返信先 $cc = ''; //CC送信先があれば入力 $bcc = ''; //BCC送信先があれば入力
57〜62行目 メール本文設定
送信されるメールの本文です。任意で編集してください。項目を追加する場合もここに追記が必要です。
// メール本文設定
$message = "お問い合わせフォームより、下記内容でメッセージが送信されました。" . "\r\n\r\n";
$message .= "名前:" . $your_name . " \r\n";
$message .= "メールアドレス:" . $your_email . " \r\n";
$message .= "住所:〒" . $your_zip . " " . $your_addr . "\r\n\r\n";
$message .= "お問い合わせ内容:\r\n" . $your_inquiry . " \r\n";
63〜77行目 メールヘッダ設定
メールヘッダ部分です。ここは特に編集する必要はないと思います。
//メールヘッダ設定
$header = "Content-Type: text/plain \r\n";
$header .= "Return-Path: " . $from_mail . " \r\n";
$header .= "From: " . $from_name . "<" . $from_mail . ">\r\n";
$header .= "Sender: " . $from_mail . " \r\n";
$header .= "Reply-To: " . $reply_to . " \r\n";
if ( !empty( $cc ) ) {
$header .= "Cc: " . $cc . " \r\n";
}
if ( !empty( $bcc ) ) {
$header .= "Bcc: " . $bcc . " \r\n";
}
$header .= "Organization: " . $from_name . " \r\n";
$header .= "X-Sender: " . $from_mail . " \r\n";
$header .= "X-Priority: 3 \r\n";
78〜82行目 メール送信
mb_send_mail
でメールを送信します。
mb_send_mail( $to, $subject, $message, $header ); $sendcheck = "send"; } } ?>
83〜85行目 送信済メッセージ表示
メール送信後に表示するメッセージです。任意で編集してください。
<?php if($sendcheck == 'send'){ ?> <p class="d-block border border-success p-2">メッセージが送信されました。</p> <p class="mx-2"> 内容確認のうえ、改めてこちらからご連絡差しあげますので、今しばらくお待ちください。</p>
86〜90行目 送信エラーメッセージ表示
トークンが一致しなかった場合のエラーメッセージです。お問い合わせフォームのページを別タブで開いてしまうと、セッションに新しいトークンが格納されてしまうため、元のページから送信しようとするとトークンが不一致となりエラーが発生します。そんなことやる人いないでしょうけど。。
<?php }elseif($sendcheck == 'failed'){ ?> <p class="d-block border border-danger p-2">メッセージが送信できませんでした。</p> <p class="mx-2"> フォームを複数タブで開いた場合、エラーが発生する可能性があります。 お手数ですがページを更新した上で再度お試しください。 </p> <?php }else{ ?>
91〜123行目 メールフォーム
HTMLのフォーム部分です。任意で編集してください。必須の項目にはrequired
を付けます。
<form class="needs-validation h-adr" method="post"> <div class="mb-3"> <label>名前<span class="text-danger ml-1">※</span></label> <input type="text" class="form-control" name="your_name" placeholder="名前" required> <div class="invalid-feedback"> 入力してください </div> </div> <div class="mb-3"> <label>Eメール<span class="text-danger ml-1">※</span></label> <input type="email" class="form-control" name="your_email" placeholder="Eメール" required> <div class="invalid-feedback">入力してください</div> </div> <div class="mb-3 row"> <div class="col-md-3"> <span class="p-country-name" style="display:none;">Japan</span> <label>郵便番号</label> <input type="text" name="your_zip" maxlength="8" class="form-control mb-2 p-postal-code" placeholder="郵便番号"> </div> <div class="col-md-9"> <label>住所</label> <input type="text" name="your_addr" class="form-control p-region p-locality p-street-address p-extended-address" placeholder="住所"> </div> </div> <div class="mb-3"> <label>お問い合わせ内容<span class="text-danger ml-1">※</span></label> <textarea class="form-control" name="your_inquiry" rows="5" placeholder="お問い合わせ内容" required></textarea> <div class="invalid-feedback">入力してください</div> </div> <div class="mb-3"> <div class="custom-control"> <input class="custom-control-input" type="checkbox" value="" id="invalidCheck" required> <label class="custom-control-label" for="invalidCheck">個人情報保護方針に同意する</label> <div class="invalid-feedback mb-3">送信する前に同意する必要があります</div> </div> </div>
124〜131行目 トークンのPOST
セッションに格納したトークンをinput type="hidden"
でPOSTします。
<input type="hidden" name="token" value="<?php echo $_SESSION['token'] ?>"> <div> <button type="submit" class="btn btn-primary mb-3">送信</button> </div> </form> <?php } ?> <!--//Mail Form--> </div>
132〜135行目 js 読み込み
jquery.js 、popper.js、 bootstrap.js、 yubinbango.jsを読み込みます。
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script> <script src="https://yubinbango.github.io/yubinbango/yubinbango.js" charset="UTF-8"></script>
136〜154行目 バリデーションの設定
バリデーションで不備があった場合、POSTしないよう制御。
<script> (function() { 'use strict'; window.addEventListener('load', function() { var forms = document.getElementsByClassName('needs-validation'); var validation = Array.prototype.filter.call(forms, function(form) { form.addEventListener('submit', function(event) { if (form.checkValidity() === false) { event.preventDefault(); event.stopPropagation(); } form.classList.add('was-validated'); }, false); }); }, false); })(); </script> </body> </html>
以上です。
多分大丈夫ですがあくまで自己責任でご使用ください。何かおかしいところあればメッセージ頂けると助かります。
- Forms – Bootstrap 4.1 日本語リファレンス
- Bootstrap4テンプレート フォーム用(HTMLバリデーション) – Laravel学習帳
- GitHub – yubinbango/yubinbango
- お問合せフォームを悪用する攻撃増加に関する注意喚起 | さくらインターネット
- 【セキュリティ ニュース】サイトの「問い合わせフォーム」を悪用する攻撃に警戒を):Security NEXT
- クロスサイトリクエストフォージェリ(CSRF) | トレンドマイクロ
- クリックジャッキングとは? 攻撃の仕組みと対策:株式会社 日立ソリューションズ・クリエイト
- 安全なウェブサイトの作り方 – 1.8 メールヘッダ・インジェクション:IPA 独立行政法人 情報処理推進機構
- 送ったメールがスパム判定(迷惑メール)されないためのヘッダー設定 | GRAYCODE PHPプログラミング
- メールの送信元が文字化けした時の対処方法:mb_encode_mimeheader関数 | GRAYCODE PHPプログラミング
- mb_send_mailでCCやBCCを指定する 表示名を指定する – [サンプルコード/PHP] ぺんたん info
- はじめに – Bootstrap 4.3 – 日本語リファレンス
- Grid system – Bootstrap 4.2 – 日本語リファレンス
- Colors – Bootstrap 4.4 – 日本語リファレンス
- PHP エスケープ処理のためのhtmlspecialcharsの使い方 | Web Development Blog
- セキュリティーを考慮したメールフォームの作り方 – Qiita
- PHPの標準関数を使ってユニーク(一意)なトークン(文字列)を生成する – Qiita