SP用メニューボタン
ソレゾレブログ

技術的な事だったり日常の気になる事だったり

お問い合わせフォームをレンタルサーバーのSMTPサーバーやWP Mail SMTPを使ってWP環境で自作する②

スポンサーリンク

目次

はじめに

この記事では、お問い合わせフォームを自作するうえで気を付けるべきセキュリティ対策について書いていきます。すべての対策について網羅しているわけではないため、対策の中の一部とお考え下さい。これ以外にも対策はありますので、自身でお調べ頂くことをお勧めします。

CSRFトークンを使ったCSRF(クロスサイトリクエストフォージェリ)対策

クロスサイトリクエストフォージェリとは

クロスサイトリクエストフォージェリ攻撃におけるお問い合わせフォームの脆弱性を簡単に説明すると、攻撃者が用意した攻撃用サイトから自サイトのお問い合わせフォームを利用してメール送信されてしまう点です。

ユーザーがお問い合わせフォームにアクセスしているとします。お問い合わせフォームはユーザーにセッションIDを発行しています。ユーザーは同時に、攻撃者が用意したWebサイトに何らかの方法で誘導されアクセスしていたとします。ユーザーは、攻撃用のWebサイトに用意されたお問い合わせなどの送信ボタンをクリックしてしまいます。そうすると、同時にアクセス中のお問い合わせフォームのメール送信機能を使って悪意ある攻撃が出来てしまうという攻撃の事を言うそうです。

CSRFについて詳しく知りたい方は下記を参照してください。

通常のユーザーは、「お問い合わせページ → 送信確認ページ → 送信及び送信完了ページ」の順に移動してお問い合わせを送信しますが、悪意のある攻撃者は、外部サイトから突然送信及び送信完了ページを利用してメール送信してしまいます。これが出来ないようにするためにCSRFトークンを使います。

スポンサーリンク

CSRFトークンを使った対策

CSRFトークンの生成と埋め込み

ユーザーがお問い合わせの入り口であるお問い合わせページに初回アクセスをしてきたら、サーバーはCSRFトークンを発行し、$_SESSION変数の「csrf-token」要素に保存します。

同時に、お問い合わせページform要素内にhiddenタイプのinput要素を作成して、そのvalue属性に生成したCSRFトークン埋め込みます。ユーザーは、form要素内にCSRFトークンが埋め込まれたお問い合わせページを受け取ります。

hiddenタイプのinput要素はソースコードを見ると簡単に見ることが出来る為、通信をSSLにより暗号化することで、CSRFトークンの傍受盗用を防ぎます。

CSRFトークンはユーザーと正規のサーバーしか持たないような仕組みであるため、CSRFトークンを持たない攻撃者が外部サーバーからアクセスしてきた場合、CSRFトークンチェックでアクセスを遮断することが出来ます。

CSRFトークンのチェックと再作成

ユーザーが対象のsubmitボタンをクリックしてページ遷移すると、hiddenタイプのinput要素に埋め込んだCSRFトークンがサーバーにPOSTされます。

サーバーは、$_SESSION変数と$_POST変数のCSRFトークンを比較して合致しなければエラー画面にリダイレクトさせます。

トークンが合致した場合は、新しいトークンを作成後にhiddenタイプのinput要素に埋め込んで、送信確認ページをユーザーに渡します。

下記がCSRFトークンチェック対象のページ遷移です。

  • お問い合わせページの「送信確認」ボタンをクリックして送信確認ページに遷移した時
  • 送信確認ページの「修正する」ボタンをクリックしてお問い合わせページに戻ったとき
  • 送信確認ページの「送信する」ボタンをクリックして送信及び送信完了ページに遷移した時
  • お問い合わせページに戻ってから「送信確認」ボタンをクリックして送信確認ページに遷移する時
  • 送信及び送信完了ページでエラー表示となって「入力画面に戻る」ボタンをクリックしてお問い合わせページに戻ったとき

CSRFトークンの制御外となるページ遷移

下記のページ遷移時は、セッションが切断されるのでCSRFトークンの生成も受け渡しも発生しません。

  • 送信及び送信完了ページで送信が成功して「トップページに戻る」ボタンでトップページに戻るとき
  • 不正アクセスエラーページが表示されて「入力画面に戻る」ボタンでお問い合わせページに戻るとき
  • セッションタイムアウトページが表示されて「入力画面に戻る」ボタンでお問い合わせページに戻るとき

CSRFトークンの実装

CSRFトークンの生成設定

CSRFトークンを生成していきます。 CSRFトークンはお問い合わせの入り口であるお問い合わせページに設定します。

<?php
/*
Template Name: contact
*/
?>

<?php
if (isset($_POST['submit-return'])) {
    my_session_start();
} else {
    my_session_destroy();
    my_session_start();
    session_regenerate_id();
    $_SESSION['csrf-token'] = sha1(random_bytes(30)); //これを追記します。
}

要素名「csrf-token」は好きに決めてください。

安全なCSRFトークンの作成方法には諸説あります。好きな方法を使ってください。下記記事が参考になると思います。

この時点でお問い合わせページにアクセスしてみると、$_SESSION変数にcsrf-token要素が追加されていると思います。

CSRFトークンを使ったCSRF(クロスサイトリクエストフォージェリ)対策

CSRFトークンをhiddenタイプのinput要素でページに埋め込む設定

value属性にCSRFトークン($_SESSION['csrf-token'])を設定したhiddenタイプのinput要素を作成して、各ページに埋め込みます。submitボタンの上に追加します。

<main class="contact">
    <h1>お問い合わせフォーム</h1>
    <form action="/confirm" method="post">
            :
        <input type="hidden" name="csrf-token" value="<?php echo $_SESSION['csrf-token']; ?>">
        <input type="submit" value="送信確認">
    </form>
</main>
<main class="confirm">
          :
    <div>
        <form action="/contact" method="post">
            <input type="hidden" name="csrf-token" value="<?php echo $_SESSION['csrf-token']; ?>">
            <input type="submit" value="修正する" name="submit-return">
        </form>
        <form action="/complete" method="post">
            <input type="hidden" name="csrf-token" value="<?php echo $_SESSION['csrf-token']; ?>">
            <input type="submit" value="送信する">
        </form>
    </div>
</main>
<main class="complete">
            :
    <?php } else { ?>
            :
        <form action="/contact" method="post">
            <input type="hidden" name="csrf-token" value="<?php echo $_SESSION['csrf-token']; ?>">
            <input type="submit" value="入力画面に戻る" name="submit-return">
        </form>
    <?php } ?>
</main>

スポンサーリンク

submitボタンによるページ遷移時のCSRFトークンチェックの設定

CSRFトークンをチェックは複数ページで必要ですので、使いまわすために関数を作成していきます。function.phpにコードを追記していきます。

function my_csrf_token_check() {
    if (!isset($_POST['csrf-token']) || isset($_POST['csrf-token']) && !hash_equals($_POST['csrf-token'], $_SESSION['csrf-token'])) {
         header('location: ' . esc_url(home_url('/contacterror')));
    } else {
        $_SESSION['csrf-token'] = sha1(random_bytes(30));
    }
}

1行目は、関数を作成しています。

2行目は、if文で下記の条件を判定しています。どちらかにマッチした場合はfalse判定でエラー処理を実行させます。

①CSRFトークンがPOSTされてこなかった。

②CSRFトークンがPOSTされてきた、且つ、POSTされてきたCSRFトークン($_POST['csrf-token'])とサーバーが持っているCSRFトークン ($_SESSION['csrf-token']) が非一致。

3行目は、2行目の条件判定でtrue(CSRFトークンチェックエラー)となったら不正アクセスエラーページにリダイレクトしています。

4行目は、2行目でfalse(CSRFトークンチェック成功)となった場合はCSRFトークンを新たに発行します。

CSRFトークンチェックを各ページに設定する

CSRFトークンチェックを各ページに設定します。

<?php
/*
Template Name: contact
*/
?>

<?php
if (isset($_POST['submit-return'])) {
    my_session_start();
    my_csrf_token_check(); //ここに追記します。
} else {
    my_session_destroy();
    my_session_start();
    session_regenerate_id();
    $_SESSION['csrf-token'] = sha1(random_bytes(30));
}
<?php
/*
Template Name: confirm
*/
?>

<?php
my_session_start();
my_csrf_token_check(); //ここに追記します。
my_get_session_exec();
?>
<?php
/*
Template Name: complete
*/
?>

<?php
my_session_start();
my_csrf_token_check(); //ここに追記します。
?>

CSRF対策の動作確認

これでCSRFトークンによるCSRF対策が完成したので、同作確認をしていきます。下記を確認して下さい。

  • お問い合わせページにアクセスすると、$_SESSION変数のcsrf-token要素が作成されていキーが格納されている。
  • お問い合わせページから送信確認ページ、送信確認ページからお問い合わせページ、送信確認ページから送信及び送信完了ページ、送信及び送信完了ページ(エラー表示)からお問い合わせページ、それぞれのsubmitボタンクリックによる移動が門がいなく出来、CSRFトークンが毎回書き変わる。
  • 送信確認ページ、送信及び送信完了ページへ突然アクセスすると不正アクセスエラーページにリダイレクトされる。

PHPセッション名の変更

PHPのセッション名は初期値で「PHPSESSID」となっています。PHPのセッション名が初期値になっていると、セッションIDを利用した攻撃者に対して、セッションIDが特定されやすくなります。PHPのセッション名は初期値のものとは違うものにすることをお勧めします。

PHPのセッション名を変更するには、php.iniの「session.name」を変更後、Webサーバーのサービスを再起動します。

session.name = 特定されにくいセッション名を設定

クリックジャッキング対策

クリックジャッキングとは、iframeを利用して表示した本物のWebページの上に、視覚的にわからないボタンやリンクをかぶせるように配置して、ユーザーにそれをクリックさせ、攻撃者が用意したスクリプトを実行させられたり、他のサイトに誘導されて悪意のある攻撃をされる事を言います。

詳しくは下記を参照してください。

対策としては、HTTPヘッダーに「X-FRAME-OPTIONS」の「SAMEORIGIN」を設定します。「X-FRAME-OPTIONS」は、外部サイトからiframeなどへのページの読み込みを制限します。SAMEORIGINは、フレーム内のページ表示を同一ドメイン内のみに制限します。下記コードをheader.phpに追記して全てのページに組み入れます。

header('X-FRAME-OPTIONS: SAMEORIGIN');

開発者モードのネットワークタブでSAMEORIGINなどと検索すると、対象ページのレスポンスヘッダーに追加されていると思います。

クリックジャッキング対策

これで、自分のサイトがifame要素のsrcに設定されて他のサイトに表示されても、エラーとなってコンテンツが表示されなくなります。

メールヘッダ・インジェクション

メールヘッダ・インジェクション攻撃は、メールの情報が記録されているヘッダーの情報を不正改竄し、意図しない送信先にメールを送ってしまう攻撃です。

詳細は下記を参照してください。

上記サイトに記載のある解決方法をひとつずつ確認していきます。

メールヘッダを固定値にして外部からの入力はすべてメール本文に出力する

ユーザーが入力フォームに入力した値をメールヘッダーに挿入しないという対策です。ヘッダー情報を追加するコードを入力欄に書かれ、ユーザーが入力した値がメールヘッダーに動的に追加されると、まさに攻撃となってしまいます。本記事の環境ではそのような実装は無いので問題ありません。

メールヘッダを固定値にできない場合ウェブアプリケーションの実行環境や言語に用意されているメール送信用APIを使用する

本記事の環境は、ユーザーが入力フォームに入力した値をメールヘッダーに挿入しない仕様なので、これについても問題ありません。

HTMLで宛先を指定しない

hiddenタイプのinput要素などに宛先やお問い合わせ内容を直接書くという実装の事です。セキュリティ的にあり得ない実装です。本記事の環境ではそのような実装はしていません。

外部からの入力の全てについて改行コードを削除する

メールのヘッダーは1項目ごとに改行が必要です。攻撃者がメールヘッダーを改竄しようとするときは、改行コード付きの値が挿入されることになります。つまり、既存入力欄に格納された値から改行コードを削除してしまえばヘッダーとして認識されなくなる、と言うことだと思います。

実際に攻撃をシミュレーションしている記事がありますので参考にしてみてください。

対策をしていきます。このお問い合わせフォームでは、お問い合わせページからSubmitして送信確認ページに移動した時にPOSTされた値を、my_set_session_input関数を使って$_SESSION変数に格納しています。この時に改行コードを削除するよう実装します。メール本文については改行が崩れるので、ここでは除外しています。環境によってはtextarea要素の改行も削除しなくてはならないかもしれません。その辺は自己判断してください。

function my_set_session_input($name) {
    $_SESSION[$name] = esc_attr($_POST[$name]);
        ↓
    $_SESSION[$name] = str_replace(array('\r\n', '\r', '\n'), '', esc_attr($_POST[$name])); //変更する
}

スポンサーリンク

セッション固定化攻撃(セッションハイジャック)

セッションIDを盗用してユーザーになりすまし悪事を働く攻撃です。セッションIDの生成規則を割り出してユーザーに成りすましたり、ネットワークを盗聴してセッションIDを傍受悪用したり、セッションIDの固定化(Session Fixation)によって、あらかじめ取得したセッションIDを何らかの方法でユーザーに渡して、ユーザーがサイトにアクセスしたところで攻撃者がユーザーに成りすまして悪事を行います。

セッションハイジャックについて詳しくは下記を参照してください。

上記サイトに記載のある解決方法をひとつづつ確認していきます。

セッションIDを推測が困難なものにする

セッションIDが時刻情報等を基に単純なアルゴリズムで生成されている場合予測が容易であるとのことですが、このお問い合わせサイトでは、PHPに用意されたセッションID生成アルゴリズムによってIDを生成しており、十分複雑性を満たしているので、これを使っている限りは特段対応は不要かと思います。

セッションIDのハッシュについて詳しくは下記を参照してください。

セッションIDをURLパラメータに格納しない

URLの引数にセッションIDを設定してユーザーとサーバー間でやり取りするということを言っていますが、この記事の例ではURLパラメータにセッションIDを設定していないので問題ありません。httpsを実装するとURLの引数も暗号化されますが、悪意のある攻撃者は外部だけに存在するわけではありません。あなたの隣の席の人が悪人であることだってあります。そういった理由で、セッションIDをURLのパラメータに設定することは避けるべきです。

ちなみに、URLにセッションIDを埋め込んだ例は下記です。

https://ドメイン/page.php?PHPSESSID=123456789

HTTPS通信で利用するCookieにはsecure属性を加える

クッキーにSecure属性を設定すると、HTTPSが実装されていないと送信されないようになります。HTTPでは、通信を傍受されると中身が簡単に見られてセッションIDなどが簡単に漏洩してしまう為、HTTPSによる通信の暗号化を実装しましょうということです。設定するには、php.iniファイルに下記を追記し、Webサービスを再起動します。

session.cookie_secure = 1

動作確認をするには、Chromeの開発者モードで、 [ネットワーク] – [対象コンテンツ] – [Cookie] を開いて、Cookie各要素のSecure欄にチェックが入っていればOKです。

HTTPS通信で利用するCookieにはsecure 属性を加える

ログイン成功後に新しくセッションを開始する

ユーザーがサイトにログインする前に攻撃者によってセッションIDが盗まれていた場合、ユーザーがログインしたあとに攻撃者が事前入手していたセッションIDを使ってセッション固定化攻撃などの悪事に利用される可能性があります。これを防ぐには、ユーザーがログインに成功した後にセッションIDを再生成して古いIDを無効化する対策が効果的です。

PHPには、セッションIDを再生成するsession_regenerate_id関数が用意されています。これをログイン成功後速やかに実行することで対策出来ます。

ログイン成功後に既存のセッションIDとは別に秘密情報を発行しページの遷移ごとにその値を確認する

セッションIDを固定値にしない

セッションIDを全てのユーザーアクセスで同じにするとセキュリティ上良くありません。セッション・ハイジャックの可能性があります。ユーザーのセッションごとに異なるセッションIDを発行するようにします。PHPのsession_start関数でセッションを作成しているのならば、セッションごとにセッションIDが異なるよう発行されるので特段気にする必要はありません。

セッションIDをCookieにセットする場合有効期限の設定に注意する

PHPの有効期限はphp.ini「session.cookie_lifetime」で設定出来、初期値は「0」です。これは、ブラウザのセッション終了時までとなっています。初期値から変えている場合は、あまり長い有効期限は設定せず、必要最低限の機関となるようにします。

スポンサーリンク

XSS(クロスサイトスクリプティング)攻撃の対策

クロスサイトスクリプティング攻撃は、攻撃者が何らかの方法で攻撃用のサイトに誘導するようなリンクを含んだリンクをお問い合わせフォームを利用してメールで送信したり、はたまたWebサイトの掲示板などに埋め込みます。それを閲覧したユーザーがリンクをクリックすることで、攻撃用のサイトに誘導され、そのサイトを閲覧操作することで情報を盗まれたりする攻撃です。

詳細については下記サイトを参照してください。

今回のお問い合わせフォームの場合は、意図しないスクリプトが埋め込前れたURLをメール受信者が受け取り、それをクリックしたことで、攻撃を受けるということになると思います。

対策としては、埋め込まれたサイトへのリンクやスクリプトを無害化します。「&」「<」「>」「”」「’」の文字をエスケープすることで、リンクやスクリプトを埋め込まれても無害化することが出来ます。

お問い合わせフォームにまつわるphp.iniのセキュリティ設定

PHPの設定ファイルphp.iniには、PHPの挙動を決める設定値が書かれており、そこにはセッションやCookieのにまつわる設定も書いてあります。php.iniの設定によってセキュリティのレベルが変わるため、セッションとセキュリティを参考に設定を見直してください。php.iniの設定変更後は、Webサービスの再起動をすることで設定値が反映されますので、忘れずにサービスの再起動をしてください。

メールの送信者なりすまし対策

メールのなりすまし対策には、「SPF」「DKIM」「DMARC」などがあります。これらについて概要を説明します。

SPFレコードによる送信ドメイン確認

SPF(Sender Policy Framework)レコードは、メールを送信したサーバーのIPアドレスと送信元ドメインが正しい組み合わせであるかチェックするためのレコードです。送信元ドメインを管理するDNSサーバーに設定します。

受信側メールサーバーがメールを受信すると、送信元メールサーバのIPアドレスと送信元ドメインをDNSに問合せに行きます。その時に、IPアドレスとドメイン名の組み合わせを返すのがSPFレコードです。これにより、受信側は迷惑メールであるか判定することが出来ます。

多くのメールサーバーはSPFレコードによる送信元のチェックを行っているので、自分たちの送信したメールが迷惑メールと判定されないようにするためには、SPFレコードの設定は必須と言っても良いでしょう。

レンタルサーバーなど使っている場合は、SPFレコードを自動的の挿入してくれるものもあります。

SPFレコードの書き方の例を下記に示します。

ドメイン. IN TXT "v=spf1 ip4:IPアドレス -all"

「v=spf1」はSPFバージョンを表していますが、現時点でSPFバージョン1しかないので必ず「v=spf1」と設定します。IPアドレスは、メールを送信するときの送信元グローバルアドレスを設定します。「-all」は、ドメインにマッチしなかったらすべてメールサーバーとして認証しないという意味です。

これはあくまで設定例です。環境によっては書き方を変える必要があるので、個々の環境に合わせて設定してください。より詳しい情報は下記などを参考にしてみてください。

DKIMによる電子署名の付与

DKIM(DomainKeys Identified Mail)は、メールを送信する際に送信元が電子署名を付与して送信し、受信サーバー側がその署名を検証して送信元をチェックする手法です。送信者は自身が持つ秘密鍵により電子署名を発行し、受信側は送信側のDNSサーバーに保管されている公開鍵を取得して電子署名を照合します。

私はDKIMを実装したことが無いので下記などを参考にしてみてください。

DMARCによる対策

DMARC(Domain-based Message Authentication、Reporting and Conformance)は、SPFレコードやDKIMによる送信元のチェックがNGであった時に、メールをどう扱うかポリシー設定できる機能です。送信元のDNSサーバーにDMARCレコードを追加することで対策が可能です。

DMARCについても私は実装したことが無いので下記などを参考にしてみてください。

お問い合わせフォームの処理フロー

本記事で作成しているお問い合わせフォームの処理フローを図にしました。ご参考までに。

お問い合わせフォームの処理フロー