Help us understand the problem. What is going on with this article?

セキュリティーを考慮したメールフォームの作り方

はじめに

最近PHPの勉強を始めました。
購入した参考書を読みながら少しずつ進めていき、メールフォームを自分で作成しました。

無事完成したため、他の人の作ったメールフォームも参考にしようと調べていたら、『PHP メールフォーム』の上位10件が勝率10%というQiitaの記事に辿り着きました。
要約すると「セキュリティー対策を考えてないものが多すぎ」といったものです。
はい、自分の作ったメールフォームも全くセキュリティー対策がされていません。ということで早速作り直すことにしました。

ざっと調べた感じだと以下の3つの脅威に対して対策すれば大丈夫なようです。

  • クリックジャッキング
  • クロスサイトリクエストフォージェリ(CSRF)
  • メールヘッダインジェクション

クリックジャッキング

クリックジャッキングとは?

クリックジャッキングとは、ユーザーが利用するWebブラウザを悪用して、ユーザーに不利益をもたらすサイバー攻撃の一種で、具体的な特徴としては、ボタンやリンクなどを透明で見えない状態にして、通常のWebページの上にかぶせてしまうというもの。「サイバーセキュリティ.comより引用」

正規のサイトの上に見えないリンクを上から被せて、悪意のあるサイトに誘導させるという感じですかね。

対策

同一オリジン(origin)以外のページ表示を禁止させます。以下のPHPコードを追加することで対策できます。

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

クロスサイトリクエストフォージェリ(CSRF)

CSRFとは?

オンラインサービスを利用するユーザーがログイン状態を保持したまま悪意のある第三者の作成したURLなどをクリックした場合などに、本人が意図しない形で情報・リクエストを送信されてしまうことを意味します。リクエスト強要とも呼ばれており、ユーザー側は何が起きたのか気が付くことなく、後から被害にあったことに気が付きます。「CyberSecurityTIMESより引用」

他のサイト(被害を受けるサイト)にリクエストを要求する動作を仕込んだ悪意のあるサイト(罠サイト)を用いて、何も知らないユーザーが攻撃することですね。これにより意図していないリクエストが送信される可能性があります。
スクリーンショット 2019-09-29 14.52.46.png

対策

トークンを利用することで、送信前に正規のリクエストかどうかの確認を行います。
以下のコードはトークンの生成にて使用しました。

// ランダムな文字列をハッシュ化する
if (!isset($_SESSION['token'])) {
    $_SESSION['token'] = sha1(random_bytes(30));
}

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

メールヘッダインジェクションとは?

メールヘッダー・インジェクションとは、問い合わせフォームなどのメールを送信する画面で、メールの内容を改ざんし、迷惑メールの送信などに悪用されてしまう脆弱性のことです。
メールの宛先に改行コードを挿入することで、新しいメールヘッダーを追加し、本来の宛先以外にメールを送信したり、メールの内容を改ざんしたりします。「Webセキュリティの小部屋より引用」

対策

対策としては2つあるようです。

  • メールヘッダー部分に入力値を使用しない。
  • 入力値の改行コードを削除する

改行コードの削除は以下のコードです。

$text = str_replace(array("\r", "\n"), '', $text);

実際に作成

以上の点に注意して実際にメールフォームを作ってみたいと思います。

index.php
<?php
session_start();

//クリックジャッキング対策
header('X-FRAME-OPTIONS: SAMEORIGIN');

// トークン生成
if (!isset($_SESSION['token'])) {
    $_SESSION['token'] = sha1(random_bytes(30));
}

// HTML特殊文字をエスケープする関数
function escape($str) {
    return htmlspecialchars($str,ENT_QUOTES,'UTF-8');
}
?>
<html>
    <head>
        <title>メールフォーム</title>
    </head>

    <body>
    <h2>お問い合わせ</h2>
        <div class="comment">
            <form action="mailConfirm.php" method="POST">
                <div class="wrapper">
                    <div class="title">Name<span>*</span></div>
                    <div class="insert">
                        <input type="text" name="name" placeholder="全角20文字以内" required>
                    </div>
                </div>

                <div class="wrapper">
                    <div class="title">Contact<span>*</span></div>
                    <div class="insert">
                        <input id="contact_mail" name="adress_mail" type="text" placeholder="Mail(半角30文字以内)">
                    </div>
                </div>

                <div class="wrapper">
                    <div class="title">Comment<span>*</span></div>
                    <div class="insert">
                        <textarea name="comment" placeholder="全角200文字以内" required></textarea>
                    </div>
                </div>
                <input type="hidden" name="token" value="<?=$_SESSION['token']?>">
                <div class="submit">
                    <input type="submit" value="送信">
                </div>
            </form>
        </div>
    </body>
</html>
mailConfirm.php
<?php
session_start();

//クリックジャッキング対策
header('X-FRAME-OPTIONS: SAMEORIGIN');

// HTML特殊文字をエスケープする関数
function escape($str) {
    return htmlspecialchars($str,ENT_QUOTES,'UTF-8');
}

//前後にある半角全角スペースを削除する関数
function spaceTrim ($str) {
    // 行頭
    $str = preg_replace('/^[  ]+/u', '', $str);
    // 末尾
    $str = preg_replace('/[  ]+$/u', '', $str);
    return $str;
}

//tokenを変数に入れる
$token = $_POST['token'];

// トークンを確認し、確認画面を表示
if(!(hash_equals($token, $_SESSION['token']) && !empty($token))) {
    echo "不正アクセスの可能性があります";
    exit();
}

//POSTされたデータを各変数に入れる
$name = isset($_POST['name']) ? $_POST['name'] : NULL;
$mail = isset($_POST['adress_mail']) ? $_POST['adress_mail'] : NULL;
$comment = isset($_POST['comment']) ? $_POST['comment'] : NULL;

//前後にある半角全角スペースを削除
$name = spaceTrim($name);
$mail = spaceTrim($mail);
$comment = spaceTrim($comment);

//名前入力判定
if ($name == ''){
    $errors['name'] = "名前が入力されていません。";
}

//メール入力判定
if ($mail == ''){
    $errors['mail'] = "メールが入力されていません。";
}

//コメント入力判定
if ($comment == ''){
    $errors['comment'] = "コメントが入力されていません。";
}

//エラーが無ければセッションに登録
if(count($errors) === 0){
    $_SESSION['name'] = $name;
    $_SESSION['mail'] = $mail;
    $_SESSION['comment'] = $comment;
}
?>

<html>
    <head>
        <title>確認ページ</title>
    </head>

    <body>
        <form method="post" action="mailSend.php">
            <table>
                <tr>
                    <th>お名前</th>
                    <td><?php echo escape($name); ?></td>
                </tr>
                <tr>
                    <th>メールアドレス</th>
                    <td><?php echo escape($mail); ?></td>
                </tr>
                <tr>
                    <th>内容</th>
                    <td><?php echo nl2br(escape($comment)); ?></td>
                </tr>
            </table>
            <input type="button" value="戻る" onClick="history.back()">
            <input type="hidden" name="token" value= <?php echo escape($token); ?>>
            <button>送信</button>
        </form>
    </body>
</html>
mailSend.php
<?php
session_start();

//クリックジャッキング対策
header('X-FRAME-OPTIONS: SAMEORIGIN');

// HTML特殊文字をエスケープする関数
function escape($str) {
    return htmlspecialchars($str,ENT_QUOTES,'UTF-8');
}

//前後にある半角全角スペースを削除する関数
function spaceTrim ($str) {
    // 行頭
    $str = preg_replace('/^[  ]+/u', '', $str);
    // 末尾
    $str = preg_replace('/[  ]+$/u', '', $str);
    return $str;
}

//tokenを変数に入れる
$token = $_POST['token'];

// トークンを確認し、確認画面を表示
if(!(hash_equals($token, $_SESSION['token']) && empty($token))) {
    echo "不正アクセスの可能性があります";
    exit();
}
?>

<html>
    <head>
        <title>送信ページ</title>
    </head>

    <body>
        <?php
            $email = 'mailAdress@gmail.com';

            mb_language('japanese');
            mb_internal_encoding('UTF-8');

            $from = $_SESSION['mail'];
            $mail = $_SESSION['mail'];
            $name = $_SESSION['name'];
            $comment = $_SESSION['comment'];

            $subject = 'お問い合わせがあったよ!';
            $body = $comment. "\n". $name. "\n". $mail;

            $success = mb_send_mail($email, $subject, $body, 'From: '.$from);
        ?>

        <div id="tyMessage">
        <?php if ($success) : ?>
        ご意見ありがとうございます!
        <?php else : ?>
        申し訳ありません。エラーのため送信できませんでした。<br>
        <?php endif; ?>
        <a href="index.php">Homeに戻る</a><br>
        </div>
    </body>
</html>

おわりに

これである程度のセキュリティーを考慮したメールフォームが作れたのではないかと思います。
最初にも述べましたが筆者は最近PHPについて勉強し始めたため、間違った記述があるかもしれません。その際は指摘をよろしくお願いします。

参考にさせて頂いたサイト様

思考の葉

【PHP】作成したメールフォームに脆弱性がないか、アドバイスもらえないでしょうか。

これで完璧!今さら振り返る CSRF 対策と同一オリジンポリシーの基礎

この記事を書く際(特にコードを書く際)に大変参考にさせて頂きました。
ありがとうございました!

vber_program
月末だけ活動する人。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away