メールフォームへセキュリティ対策の適用

前回は一般的なアタック手法についてご説明しましたので、当記事では実際のプログラムに対策を適用していきます。
実務でもセキュリティに配慮したプログラミングは重要ですので、これを期にぜひマスターしましょう。

変数のホワイトリスト化

送信された値の中から、プログラムで使用する変数を明示することで、意図しない変数が紛れ込まないようにする手法です。

対策をしない場合のリスク

以下は、今までのメール送信プログラムから、『お名前』の項目を削除しようとした例です。HTMLからは削除したのですが、前半のロジックの部分から『お名前』を消すのを忘れてしまったとしましょう。
(<!– xxxx –>というのは、HTMLを部分的にコメントアウトする記法です。これで、画面には『お名前』の行が表示されなくなりました)

inquiry.php
<?php
$page_message = ""; // ページに表示するメッセージ
if (isset($_REQUEST["send"])) {
    // 初期設定
    mb_language("japanese"); // メール送信の際のおまじない
    mb_internal_encoding("UTF-8"); // メール送信の際のおまじない
    
    // 送信本文の作成
    $mail_body = "";
    if (isset($_REQUEST["uname"])) {
        $mail_body .= "[お名前]\n";
        $mail_body .= "{$_REQUEST["uname"]}\n";
    }
    if (isset($_REQUEST["email"])) {
        $mail_body .= "[メールアドレス]\n";
        $mail_body .= "{$_REQUEST["email"]}\n";
    }
    if (isset($_REQUEST["body"])) {
        $mail_body .= "[お問い合わせ内容]\n";
        $mail_body .= "{$_REQUEST["body"]}\n";
    }
    
    // 送信実行
    $subject = "お問い合わせがありました";
    $admin_email = "you@example.com"; // あなたのメールアドレスを入力してください
    $add_header = "From:" . $admin_email;
    $result = mb_send_mail($admin_email, $subject, $mail_body, $add_header);
    
    // 完了
    $page_message = "送信しました!";
}
?>

<?php $page_title = "お問い合わせ";?>
<?php require "header.php";?>
    <p>
      <?php echo $page_message; ?>
    </p>
    <p>
      お問い合わせは以下よりお願いします
    </p>
    <form action="inquiry.php" method="post">
      <!--
      <div>
        お名前<br>
        <input type="text" name="uname" size="30">
      </div>
      -->
      <div>
        メールアドレス<br>
        <input type="text" name="email" size="30">
      </div>
      <div>
        お問い合わせ内容<br>
        <textarea name="body" rows="5" cols="20"></textarea>
      </div>
      <div>
        <input type="submit" name="send" value="送信する">
      </div>
    </form>
<?php require "footer.php";?>

上記では、プログラム部分の『お名前』のロジックが残っています。

    if (isset($_REQUEST["uname"])) {
        $mail_body .= "[お名前]\n";
        $mail_body .= "{$_REQUEST["uname"]}\n";
    }

このため、『偽装した通信内容を送り込まれると、お名前(uname)を検出して、プログラムの挙動に反映されてしまう』という問題が起こります。
本質的には、個人情報漏洩や致命的なリスクにつながる脆弱性となり得ます。

対策実施

ホワイトリストを定義し、プログラムで利用する変数を明確化します。
プログラム前半を以下のように修正することで、$requestという配列に、許可された変数だけを格納するようになります。(send,email,bodyの添え字しかプログラムで利用しない)

<?php
// ホワイトリスト変数の作成
$whitelist = array("send", "email", "body");
$request = array(); // 配列の初期化
foreach ($whitelist as $word) { // $whitelistの中身を繰り返し
    $request[$word] = null; // nullという空白値を初期値にする
    if (isset($_REQUEST[$word])) { // 送信されてきた値の存在確認
        $request[$word] = $_REQUEST[$word]; // 明示された変数のみ$requestに格納
    }
}

$page_message = ""; // ページに表示するメッセージ
if (isset($request["send"])) {
    // 初期設定
    mb_language("japanese"); // メール送信の際のおまじない
    mb_internal_encoding("UTF-8"); // メール送信の際のおまじない
    
    // 送信本文の作成
    $mail_body = "";
    if (isset($request["uname"])) {
        $mail_body .= "[お名前]\n";
        $mail_body .= "{$request["uname"]}\n";
    }
    if (isset($request["email"])) {
        $mail_body .= "[メールアドレス]\n";
        $mail_body .= "{$request["email"]}\n";
    }
    if (isset($request["body"])) {
        $mail_body .= "[お問い合わせ内容]\n";
        $mail_body .= "{$request["body"]}\n";
    }
    
    // 送信実行
    $subject = "お問い合わせがありました";
    $admin_email = "you@example.com"; // あなたのメールアドレスを入力してください
    $add_header = "From:" . $admin_email;
    $result = mb_send_mail($admin_email, $subject, $mail_body, $add_header);
    
    // 完了
    $page_message = "送信しました!";
}
?>

※foreachという構文については、後ほど解説します。まずは、『配列の中身を取り出しながら繰り返すもの』として理解しておいてください。

サニタイズ(サニタイジング)

インターネットから送信されてくる値の中には、攻撃者がアタックコードを混入させた値が含まれる場合があります。
攻撃手法としては、前記事の[中級]Webアプリケーションの脆弱性と対策でも触れていますが、『クロスサイトスクリプティング』『セッションハイジャック』『クロスサイトリクエストフォージェリ』が該当します。
サニタイズを行うと、変数を無害化することで、不具合やセキュリティリスクを防ぐことができます。
今回は、HTML上に変数を表示する際の対策についてご紹介します。

対策をしない場合のリスク

以下は、今までのメール送信プログラムに機能追加をし、メール送信後に『○○さん、送信ありがとうございました!』と表示するように修正したケースです。
(お名前は復活させました)

inquiry.php
<?php
// ホワイトリスト変数の作成
$whitelist = array("send", "uname", "email", "body");
$request = array(); // 配列の初期化
foreach ($whitelist as $word) { // $whitelistの中身を繰り返し
    $request[$word] = null; // nullという空白値を初期値にする
    if (isset($_REQUEST[$word])) { // 送信されてきた値の存在確認
        $request[$word] = $_REQUEST[$word]; // 明示された変数のみ$requestに格納
    }
}

$page_message = ""; // ページに表示するメッセージ
if (isset($request["send"])) {
    // 初期設定
    mb_language("japanese"); // メール送信の際のおまじない
    mb_internal_encoding("UTF-8"); // メール送信の際のおまじない
    
    // 送信本文の作成
    $mail_body = "";
    if (isset($request["uname"])) {
        $mail_body .= "[お名前]\n";
        $mail_body .= "{$request["uname"]}\n";
    }
    if (isset($request["email"])) {
        $mail_body .= "[メールアドレス]\n";
        $mail_body .= "{$request["email"]}\n";
    }
    if (isset($request["body"])) {
        $mail_body .= "[お問い合わせ内容]\n";
        $mail_body .= "{$request["body"]}\n";
    }
    
    // 送信実行
    $subject = "お問い合わせがありました";
    $admin_email = "you@example.com"; // あなたのメールアドレスを入力してください
    $add_header = "From:" . $admin_email;
    $result = mb_send_mail($admin_email, $subject, $mail_body, $add_header);
    
    // 完了
    $page_message = $request["uname"] . "さん、送信ありがとうございました!";
}
?>

<?php $page_title = "お問い合わせ";?>
<?php require "header.php";?>
    <p>
      <?php echo $page_message; ?>
    </p>
    <p>
      お問い合わせは以下よりお願いします
    </p>
    <form action="inquiry.php" method="post">
      <div>
        お名前<br>
        <input type="text" name="uname" size="30">
      </div>
      <div>
        メールアドレス<br>
        <input type="text" name="email" size="30">
      </div>
      <div>
        お問い合わせ内容<br>
        <textarea name="body" rows="5" cols="20"></textarea>
      </div>
      <div>
        <input type="submit" name="send" value="送信する">
      </div>
    </form>
<?php require "footer.php";?>

上記においては、

    $page_message = $request["uname"] . "さん、送信ありがとうございました!";

で変数を定義し、

    <p>
      <?php echo $page_message; ?>
    </p>

で画面表示を行っているわけですが、これには大きな問題があります。
それでは、クロスサイトスクリプティングのリスクを体験するための実験を行いますので、ローカル開発環境で以下をお試しください。

クロスサイトスクリプティングの実験

inquiry.phpを前述のものにして、お名前欄に、『<script>alert(“脆弱性!”);</script>』と入力して送信してみてください。
すると、次のようなポップアップが表示されるはずです。

step2-020-2

任意のJavascriptコードを実行してしまいました!!
これは、クロスサイトスクリプティングの標的になりうる状況です。
実際の攻撃では、外部の掲示板やSNSや攻撃用サイトから、アタックコードを埋め込んだURLを設置されることになります。このアタック用URLを踏んだユーザーは、セッションの奪取や強制アクセスなどの被害を受けることになります。

対策実施

今回は、文字列を出力する際にサニタイズを行う方法で、対策したいと思います。
具体的には、htmlentitiesという関数を利用します。

    <p>
      <?php echo htmlentities($page_message, ENT_QUOTES, "UTF-8"); ?>
    </p>

上記のようにすることで、文字列はHTMLエンティティに変換され、安全に画面出力を行えるようになります。
第二引数の『ENT_QUOTES』は、シングルクォートを変換対象にすることを意味し、
第三引数の『UTF-8』は、文字コードがUTF-8であることを意味しています。
※前者のENT_QUOTESは定数ですので、ダブルクォートやシングルクォートで囲む必要はありません。

step2-020-3

なお、htmlentitiesについての詳細は、以下をご覧ください。

PHPマニュアル htmlentities

HTMLエンティティについて

HTMLエンティティとは、『<、>、&、”』などのHTML上で意味のある文字を、安全な形式に置き換えた文字列になります。
上記はそれぞれ、『&lt;、&gt;、&amp;、&#34;』と置き換えられることになります。
HTMLソースコード上のHTMLエンティティは、実際の画面表示としては、『<、>、&、”』として表示されます。

バリデート(バリデーション)

Webアプリケーションに設置されたフォームにおいては、様々な情報の入力を受け付けます。
しかし、ユーザーが入力してくる値は、アプリケーションで想定しているものとは限りません。
そこで変数のバリデートを行うことで、入力された値が想定している形式であるかをチェックすることができます。

対策をしない場合のリスク

メールの送信者に対して、受信した旨のメールを返信するものとしましょう。

    // 送信実行
    $subject = "お問い合わせがありました";
    $admin_email = "you@example.com"; // あなたのメールアドレスを入力してください
    $add_header = "From:" . $admin_email;
    $result = mb_send_mail($admin_email, $subject, $mail_body, $add_header);

    // ユーザーへ送信実行
    $subject = "お問い合わせありがとうございました";
    $admin_email = "you@example.com"; // あなたのメールアドレスを入力してください
    $add_header = "From:" . $admin_email;
    $mail_to = $request["email"];
    $result = mb_send_mail($mail_to, $subject, $mail_body, $add_header);

上記のように、管理者向けのメールを送信したあとに、ユーザーにもメールを送信するものとします。
この場合のリスクとしては、『メールアドレス形式として誤った文字列を受け付けてしまう』『メールシステムへアタックコードが渡されてしまう』といったものがあります。
特に問題は、

$mail_to = $request["email"];

として、ユーザーの入力値をそのままメール送信先の変数に指定している箇所です。

対策実施

入力必須チェックと正規表現を利用して、メールアドレスの正当性を確認します。

入力必須チェック

メールアドレスの項目を必須入力扱いにしたいと思います。
そのため、以下のように修正します。

inquiry.php
<?php
// ホワイトリスト変数の作成
$whitelist = array("send", "uname", "email", "body");
$request = array(); // 配列の初期化
foreach ($whitelist as $word) { // $whitelistの中身を繰り返し
    $request[$word] = null; // nullという空白値を初期値にする
    if (isset($_REQUEST[$word])) { // 送信されてきた値の存在確認
        $request[$word] = $_REQUEST[$word]; // 明示された変数のみ$requestに格納
    }
}

$page_message = ""; // ページに表示するメッセージ
$page_error = ""; // エラーメッセージ

// エラーチェック
if (isset($request["send"])) {
    if ($request["email"] == "") {
        $page_error = "メールアドレスを入力してください\n";
    }
}

// 送信実行
if (isset($request["send"]) && $page_error == "") {
    // 初期設定
    mb_language("japanese"); // メール送信の際のおまじない
    mb_internal_encoding("UTF-8"); // メール送信の際のおまじない
    
    // 送信本文の作成
    $mail_body = "";
    if (isset($request["uname"])) {
        $mail_body .= "[お名前]\n";
        $mail_body .= "{$request["uname"]}\n";
    }
    if (isset($request["email"])) {
        $mail_body .= "[メールアドレス]\n";
        $mail_body .= "{$request["email"]}\n";
    }
    if (isset($request["body"])) {
        $mail_body .= "[お問い合わせ内容]\n";
        $mail_body .= "{$request["body"]}\n";
    }
    
    // 送信実行
    $subject = "お問い合わせがありました";
    $admin_email = "you@example.com"; // あなたのメールアドレスを入力してください
    $add_header = "From:" . $admin_email;
    $result = mb_send_mail($admin_email, $subject, $mail_body, $add_header);
    
    // 完了
    $page_message = $request["uname"] . "さん、送信ありがとうございました!";
}
?>

<?php $page_title = "お問い合わせ";?>
<?php require "header.php";?>
    <p>
      <?php echo htmlentities($page_message, ENT_QUOTES, "UTF-8"); ?>
    </p>
    <p class="attention">
      <?php echo htmlentities($page_error, ENT_QUOTES, "UTF-8"); ?>
    </p>
    <p>
      お問い合わせは以下よりお願いします
    </p>
    <form action="inquiry.php" method="post">
      <div>
        お名前<br>
        <input type="text" name="uname" size="30">
      </div>
      <div>
        メールアドレス <span class="attention">[必須]</span><br>
        <input type="text" name="email" size="30">
      </div>
      <div>
        お問い合わせ内容<br>
        <textarea name="body" rows="5" cols="20"></textarea>
      </div>
      <div>
        <input type="submit" name="send" value="送信する">
      </div>
    </form>
<?php require "footer.php";?>
style.css
body{
  padding: 10px;
}
h1{
  border-bottom: 2px solid #333333;
  margin-top: 0;
  margin-bottom: 20px;
}
.attention{
  color: red;
}

上記のように編集すると、フォームを送信する際に必須入力チェックが動作します。

step2-020-4

それでは、それぞれの行について説明します。

$page_error = ""; // エラーメッセージ

// エラーチェック
if (isset($request["send"])) {
    if ($request["email"] == "") {
        $page_error = "メールアドレスを入力してください\n";
    }
}

上記では、送信実行モードの際に、emailが空白であるかをチェックしています。
空白だった場合に、エラーメッセージ変数$page_errorをセットしています。

if (isset($request["send"]) && $page_error == "") {

上記では、従来の送信モードチェックに加え、『$page_errorが空白であること』という条件を追加しています。『&&』は、左右の条件を両方満たすことが求められますので、エラーが発生している場合は、送信モードをスキップする動作となります。

    <p class="attention">
      <?php echo htmlentities($page_error, ENT_QUOTES, "UTF-8"); ?>
    </p>

上記では、エラーメッセージをサニタイズした上で表示しています。
システムでセットされる変数についても、ちょっとしたミスでセキュリティホールになりえます。変数を画面表示する際は、必ずサニタイズをすることが推奨されます。

        メールアドレス <span class="attention">[必須]</span><br>

上記では、メールアドレス項目に必須である旨を表示しています。

.attention{
  color: red;
}

警告表示などを赤字にするため、スタイルを追加しています。

正規表現での形式チェック

必須入力チェックに加え、メールアドレスの形式チェック行うため、正規表現を行います。
正規表現とは、『文字形式のルールを定義することで、ある文字列がルールに則っているものか』をチェックするためのものです。たとえば、以下のようなケースに利用できます。

  • 電話番号のチェック(数字とハイフンのみ)
  • メールアドレスのチェック(一般的なメールアドレス利用文字のみ)
  • ログインIDのチェック(英数字のみ)
  • パスワード堅牢性チェック(一定文字以上+英数字含む)

正規表現でのチェックには、preg_matchという関数を利用します。
それではさっそく、以下のようにコードを修正します。

inquiry.php
// エラーチェック
if (isset($request["send"])) {
    if ($request["email"] == "") {
        $page_error = "メールアドレスを入力してください\n";
    }
    if ($page_error == "") {
        if (!preg_match('/^([a-zA-Z0-9\.\_\-\+\?\#\&\%])*@([a-zA-Z0-9\_\-])+([a-zA-Z0-9\.\_\-]+)+$/', $request["email"])) {
            $page_error = "メールアドレスを正しく入力してください\n";
        }
    }
}

上記では、必須入力チェックが終了したあと、続けてpreg_matchによりメールアドレス形式のチェックを行っています。

preg_matchの詳細については、以下をご確認ください。

PHPマニュアル preg_match

正規表現の詳細については、以下をご確認ください。

wikipedia 正規表現

ここで注意ですが、今回例としたメールアドレスについては、非常に様々な形式があるため、上記は完全な正規表現とは言えません。必要に応じて、正規表現の条件をカスタマイズしてご利用ください。
(とはいえ上記でも、実用上不便はない範囲で、入力ミスをチェックするような役には立ちます)

エラー時の初期表示をセット

セキュリティ対策とは少々離れますが、現在はフォーム入力にエラーが発生すると、毎回すべての項目を入力しなおす必要があります。
そこで、エラーで戻ってきた際は、前回の入力内容が維持されるようにします。

inquiry.php
<?php
// htmlentitiesのショートカット関数
function he($str){
    return htmlentities($str, ENT_QUOTES, "UTF-8");
}

// ホワイトリスト変数の作成
$whitelist = array("send", "uname", "email", "body");
$request = array(); // 配列の初期化
foreach ($whitelist as $word) { // $whitelistの中身を繰り返し
    $request[$word] = null; // nullという空白値を初期値にする
    if (isset($_REQUEST[$word])) { // 送信されてきた値の存在確認
        $request[$word] = $_REQUEST[$word]; // 明示された変数のみ$requestに格納
    }
}

$page_message = ""; // ページに表示するメッセージ
$page_error = ""; // エラーメッセージ

// エラーチェック
if (isset($request["send"])) {
    if ($request["email"] == "") {
        $page_error = "メールアドレスを入力してください\n";
    }
    if ($page_error == "") {
        if (!preg_match('/^([a-zA-Z0-9\.\_\-\+\?\#\&\%])*@([a-zA-Z0-9\_\-])+([a-zA-Z0-9\.\_\-]+)+$/', $request["email"])) {
            $page_error = "メールアドレスを正しく入力してください\n";
        }
    }
}

// 送信実行
if (isset($request["send"]) && $page_error == "") {
    // 初期設定
    mb_language("japanese"); // メール送信の際のおまじない
    mb_internal_encoding("UTF-8"); // メール送信の際のおまじない
    
    // 送信本文の作成
    $mail_body = "";
    if (isset($request["uname"])) {
        $mail_body .= "[お名前]\n";
        $mail_body .= "{$request["uname"]}\n";
    }
    if (isset($request["email"])) {
        $mail_body .= "[メールアドレス]\n";
        $mail_body .= "{$request["email"]}\n";
    }
    if (isset($request["body"])) {
        $mail_body .= "[お問い合わせ内容]\n";
        $mail_body .= "{$request["body"]}\n";
    }
    
    // 送信実行
    $subject = "お問い合わせがありました";
    $admin_email = "you@example.com"; // あなたのメールアドレスを入力してください
    $add_header = "From:" . $admin_email;
    $result = mb_send_mail($admin_email, $subject, $mail_body, $add_header);
    
    // 完了
    $page_message = $request["uname"] . "さん、送信ありがとうございました!";
}
?>

<?php $page_title = "お問い合わせ";?>
<?php require "header.php";?>
    <p>
      <?php echo he($page_message); ?>
    </p>
    <p class="attention">
      <?php echo he($page_error); ?>
    </p>
    <p>
      お問い合わせは以下よりお願いします
    </p>
    <form action="inquiry.php" method="post">
      <div>
        お名前<br>
        <input type="text" name="uname" size="30" value="<?php echo he($request["uname"]); ?>">
      </div>
      <div>
        メールアドレス <span class="attention">[必須]</span><br>
        <input type="text" name="email" size="30" value="<?php echo he($request["email"]); ?>">
      </div>
      <div>
        お問い合わせ内容<br>
        <textarea name="body" rows="5" cols="20"><?php echo he($request["body"]); ?></textarea>
      </div>
      <div>
        <input type="submit" name="send" value="送信する">
      </div>
    </form>
<?php require "footer.php";?>

上記のように編集すると、フォームを送信する際エラーとなった場合、初期値が入力項目にセットされるようになります。

step2-020-5

それでは修正箇所を個別に説明していきます。

// htmlentitiesのショートカット関数
function he($str){
    return htmlentities($str, ENT_QUOTES, "UTF-8");
}

上記では、heというユーザー関数を定義しています。htmlentitiesは引数の指定も含めると煩雑になるため、he関数の追加でコーディングが簡単になりました。

        <input type="text" name="uname" size="30" value="<?php echo he($request["uname"]); ?>">
        <textarea name="body" rows="5" cols="20"><?php echo he($request["body"]); ?></textarea>

inputタグのvalue属性により、入力項目の初期値をセットしています。
textareaタグについては、閉じタグの前に初期値を挟み込む形でセットしています。

ヌルバイト除去

PHP、およびPHP自体の開発に使われているC言語では、ヌルバイト(\0)を文字列の終端として扱う場合があります。
そのためヌルバイトが変数に混入すると、予期せぬ動作を引き起こします。

対策をしない場合のリスク

ヌルバイトの除去を行わないと、『文字列処理時の予期せぬ動作』『ミドルウェアの予期せぬ動作』などが発生し、セキュリティリスクになります。

対策実施

以下のようにコードを修正します。

inquiry.php
// ホワイトリスト変数の作成
$whitelist = array("send", "uname", "email", "body");
$request = array(); // 配列の初期化
foreach ($whitelist as $word) { // $whitelistの中身を繰り返し
    $request[$word] = null; // nullという空白値を初期値にする
    if (isset($_REQUEST[$word])) { // 送信されてきた値の存在確認
        $word = str_replace("\0", "", $word); // ヌルバイト除去
        $request[$word] = $_REQUEST[$word]; // 明示された変数のみ$requestに格納
    }
}

この対策により、プログラムで利用する変数からヌルバイトを除去できるようになりました。

まとめ

当記事では、メールフォームのセキュリティ対策を中心に説明しました。
現状の最新のソースコードを添付しておきますので、動作確認などをしてみてください。

step2-020.zip

このページをシェア Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedIn