1
1

PHP フォームセキュリティの学習振り返り FWなし

Last updated at Posted at 2024-03-23

XSS (Cross-Site-Scripting) 対策

対策方法: サニタイズをする。

phpの標準関数 htmlspecialcharsを使用する。

下記のコードはサニタイズを施していない。

form.php
<?php

$pageFlag = 0; // 入力画面のフラグ(Topのフォーム画面)

// $_POST['btn_confirm']が空でなければ $psteFlag = 1 に切り替える
if (!empty($_POST['btn_confirm'])) {
    $pageFlag = 1;
}

// $_POST['btn_submit']が空でなければ $psteFlag = 2 に切り替える
if (!empty($_POST['btn_submit'])) {
    $pageFlag = 2;
}
?>
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <!-- フォームに入力された確認画面の作成 -->
    <?php if ($pageFlag === 1) : ?>
        <form method="POST" action="form.php">
            氏名
            <?php echo $_POST['name']; ?>
            <br>
            メールアドレス
            <?php echo $_POST['email']; ?>
            <br>
            <!-- name属性の 'back' は $pageFlagの定義がないので 初期設定の '$pageFlag = 0;' になる -->
            <input type="submit" name="back" value="戻る">
            <!-- 完了ページへデータを送信する為のボタン($pageFlag = 2) -->
            <input type="submit" name="btn_submit" value="送信する">
            <!-- 各入力データの保持 hiddenの使用 -->
            <input type="hidden" name="name" value="<?php echo $_POST['name']; ?>">
            <input type="hidden" name="email" value="<?php echo $_POST['email']; ?>">
        </form>
    <?php endif; ?>

    <!-- 確認画面から送信後に遷移する完了画面の作成 -->
    <?php if ($pageFlag === 2) : ?>
        送信が完了しました。
    <?php endif; ?>

    <!-- フォーム入力画面を作成する(TOP) -->
    <?php if ($pageFlag === 0) : ?>
        <form method="POST" action="form.php">
            氏名
            <!-- もし $_POST['name']が空でなければ、入力データを表示 -->
            <input type="text" name="name" value="<?php if (!empty($_POST['name'])) {
                                                        echo $_POST['name'];
                                                    } ?>">
            <br>
            メールアドレス
            <!-- もし $_POST['email']が空でなければ、入力データを表示 -->
            <input type="email" name="email" value="<?php if (!empty($_POST['email'])) {
                                                        echo $_POST['email'];
                                                    } ?>">
            <br>
            <!-- btn_confirm(確認ページ)に入力データが送信されるようにする -->
            <input type="submit" name="btn_confirm" value="確認する">
        </form>
    <?php endif; ?>
</body>

</html>

下記のように入力して確認ボタンを押下してみる。

スクリーンショット 2024-03-23 16.32.45.png

下記のようにアラートが出てしまう。

スクリーンショット 2024-03-23 16.35.23.png

サニタイズされたコードを試す。

form
<?php

$pageFlag = 0; // 入力画面のフラグ(Topのフォーム画面)

// $_POST['btn_confirm']が空でなければ $psteFlag = 1 に切り替える
if (!empty($_POST['btn_confirm'])) {
    $pageFlag = 1;
}

// $_POST['btn_submit']が空でなければ $psteFlag = 2 に切り替える
if (!empty($_POST['btn_submit'])) {
    $pageFlag = 2;
}

// サニタイズ htmlspecialchars関数
function h($str)
{
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
?>
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <!-- フォームに入力された確認画面の作成 -->
    <?php if ($pageFlag === 1) : ?>
        <form method="POST" action="form.php">
            氏名
            <?php echo h($_POST['name']); ?>
            <br>
            メールアドレス
            <?php echo h($_POST['email']); ?>
            <br>
            <!-- name属性の 'back' は $pageFlagの定義がないので 初期設定の '$pageFlag = 0;' になる -->
            <input type="submit" name="back" value="戻る">
            <!-- 完了ページへデータを送信する為のボタン($pageFlag = 2) -->
            <input type="submit" name="btn_submit" value="送信する">
            <!-- 各入力データの保持 hiddenの使用 -->
            <input type="hidden" name="name" value="<?php echo h($_POST['name']); ?>"> <!-- サニタイズ -->
            <input type="hidden" name="email" value="<?php echo h($_POST['email']); ?>"> <!-- サニタイズ -->
        </form>
    <?php endif; ?>

    <!-- 確認画面から送信後に遷移する完了画面の作成 -->
    <?php if ($pageFlag === 2) : ?>
        送信が完了しました。
    <?php endif; ?>

    <!-- フォーム入力画面を作成する(TOP) -->
    <?php if ($pageFlag === 0) : ?>
        <form method="POST" action="form.php">
            氏名
            <!-- もし $_POST['name']が空でなければ、入力データを表示 -->
            <input type="text" name="name" value="<?php if (!empty($_POST['name'])) {
                                                        echo h($_POST['name']);
                                                    } ?>"> <!-- サニタイズ -->
            <br>
            メールアドレス
            <!-- もし $_POST['email']が空でなければ、入力データを表示 -->
            <input type="email" name="email" value="<?php if (!empty($_POST['email'])) {
                                                        echo h($_POST['email']);
                                                    } ?>"> <!-- サニタイズ -->
            <br>
            <!-- btn_confirm(確認ページ)に入力データが送信されるようにする -->
            <input type="submit" name="btn_confirm" value="確認する">
        </form>
    <?php endif; ?>
</body>

</html>

echoされている箇所に h関数を実行させる。

そうするとalertは発生せずに文字列としてデータは送信されるために安全である。

結果は以下

スクリーンショット 2024-03-23 17.10.16.png

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

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

Webサイト上に隠蔽・偽装したリンクやボタンを設置し、サイト訪問者を視覚的に騙してクリックさせるなど意図しない操作をするよう誘導させる手法です。

下記のような被害を被る事がある。

  • 意図しない商品を購入させられる

  • 送金させられる、ネットショップのギフト券を送付させられる

  • SNSで特定のアカウントをフォローさせられる、投稿させられる

  • Adobe Flashを使ってWebカメラやマイクを作動させられる

  • パソコンやSNSのアカウントが第三者への攻撃の踏み台にされる

  • Webサイト運営者は管理画面などを盗み見される

  • ウイルスを感染させられる

  • パソコンを乗っ取られる

対策方法: header関数を使用する

form.php
<?php

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

$pageFlag = 0; // 入力画面のフラグ(Topのフォーム画面)

// $_POST['btn_confirm']が空でなければ $psteFlag = 1 に切り替える
if (!empty($_POST['btn_confirm'])) {
    $pageFlag = 1;
}

// $_POST['btn_submit']が空でなければ $psteFlag = 2 に切り替える
if (!empty($_POST['btn_submit'])) {
    $pageFlag = 2;
}

// サニタイズ htmlspecialchars関数
function h($str)
{
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
?>
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <!-- フォームに入力された確認画面の作成 -->
    <?php if ($pageFlag === 1) : ?>
        <form method="POST" action="form.php">
            氏名
            <?php echo h($_POST['name']); ?>
            <br>
            メールアドレス
            <?php echo h($_POST['email']); ?>
            <br>
            <!-- name属性の 'back' は $pageFlagの定義がないので 初期設定の '$pageFlag = 0;' になる -->
            <input type="submit" name="back" value="戻る">
            <!-- 完了ページへデータを送信する為のボタン($pageFlag = 2) -->
            <input type="submit" name="btn_submit" value="送信する">
            <!-- 各入力データの保持 hiddenの使用 -->
            <input type="hidden" name="name" value="<?php echo h($_POST['name']); ?>"> <!-- サニタイズ -->
            <input type="hidden" name="email" value="<?php echo h($_POST['email']); ?>"> <!-- サニタイズ -->
        </form>
    <?php endif; ?>

    <!-- 確認画面から送信後に遷移する完了画面の作成 -->
    <?php if ($pageFlag === 2) : ?>
        送信が完了しました。
    <?php endif; ?>

    <!-- フォーム入力画面を作成する(TOP) -->
    <?php if ($pageFlag === 0) : ?>
        <form method="POST" action="form.php">
            氏名
            <!-- もし $_POST['name']が空でなければ、入力データを表示 -->
            <input type="text" name="name" value="<?php if (!empty($_POST['name'])) {
                                                        echo h($_POST['name']);
                                                    } ?>"> <!-- サニタイズ -->
            <br>
            メールアドレス
            <!-- もし $_POST['email']が空でなければ、入力データを表示 -->
            <input type="email" name="email" value="<?php if (!empty($_POST['email'])) {
                                                        echo h($_POST['email']);
                                                    } ?>"> <!-- サニタイズ -->
            <br>
            <!-- btn_confirm(確認ページ)に入力データが送信されるようにする -->
            <input type="submit" name="btn_confirm" value="確認する">
        </form>
    <?php endif; ?>
</body>

</html>

上部に header('X-FRAME-OPTIONS:DENY'); を記述する。

Chromeの検証ツールの Networkform.phpHeaders -> X-Frame-Options: DENY になっていればクリックジャッキング対策されている。

スクリーンショット 2024-03-23 17.23.42.png

CSRF (Cross-Site-Request-Forgeries) 対策

CSRFとは :

偽物の inputから情報が来ているのか本物の inputから情報が見分ける為の
合言葉のようなイメージらしい。

対策方法 :

  1. Session を使用する。POST送信とGET送信とは違い、情報は残る。

  2. random_bytes(32) 関数を使用する
    暗号論理的に安全な、疑似ランダムなバイト列を生成する 32 は 32バイト分の生成をする。

  3. bin2hex 関数を使って16進数に変換する)。

form.php
<?php

// sessionを使えるようにする
session_start();

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

// トークンが発行されているかの確認
if (!empty($_SESSION)) {
    echo '<pre>';
    var_dump($_SESSION);
    echo '</pre>';
}

$pageFlag = 0; // 入力画面のフラグ(Topのフォーム画面)

// $_POST['btn_confirm']が空でなければ $psteFlag = 1 に切り替える
if (!empty($_POST['btn_confirm'])) {
    $pageFlag = 1;
}

// $_POST['btn_submit']が空でなければ $psteFlag = 2 に切り替える
if (!empty($_POST['btn_submit'])) {
    $pageFlag = 2;
}

// サニタイズ htmlspecialchars関数
function h($str)
{
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
?>
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
        <!-- フォームに入力された確認画面の作成 -->
        <?php if ($pageFlag === 1) : ?>
        <!-- 発行されたトークンと送信されてきたトークンが一致するかどうか? -->
        <?php if ($_POST['csrf'] === $_SESSION['csrfToken']) : ?>
            <form method="POST" action="form.php">
                氏名
                <?php echo h($_POST['name']); ?>
                <br>
                メールアドレス
                <?php echo h($_POST['email']); ?>
                <br>
                <!-- name属性の 'back' は $pageFlagの定義がないので 初期設定の '$pageFlag = 0;' になる -->
                <input type="submit" name="back" value="戻る">
                <!-- 完了ページへデータを送信する為のボタン($pageFlag = 2) -->
                <input type="submit" name="btn_submit" value="送信する">
                <!-- 各入力データの保持 hiddenの使用 -->
                <input type="hidden" name="name" value="<?php echo h($_POST['name']); ?>"> <!-- サニタイズ -->
                <input type="hidden" name="email" value="<?php echo h($_POST['email']); ?>"> <!-- サニタイズ -->
                <!-- トークンの保持 -->
                <input type="hidden" name="csrf" value="<?php echo h($_POST['csrf']); ?>"> <!-- サニタイズ -->
            </form>
        <?php endif; ?>
    <?php endif; ?>

    <!-- 確認画面から送信後に遷移する完了画面の作成 -->
    <?php if ($pageFlag === 2) : ?>
        送信が完了しました。
        <!-- sessionトークンの削除 -->
        <?php unset($_SESSION['csrfToken']);  ?>
    <?php endif; ?>

    <!-- トークンが発行されていないときはトークンを発行する -->
    <?php if ($pageFlag === 0) : ?>
        <?php if (!isset($_SESSION['csrfToken'])) {
            $csrfToken = bin2hex(random_bytes(32));
            $_SESSION['csrfToken']= $csrfToken;
        }
        $token = $_SESSION['csrfToken'];
        ?>
        <!-- フォーム入力画面を作成する(TOP) -->
        <form method="POST" action="form.php">
            氏名
            <!-- もし $_POST['name']が空でなければ、入力データを表示 -->
            <input type="text" name="name" value="<?php if (!empty($_POST['name'])) {
                                                        echo h($_POST['name']);
                                                    } ?>"> <!-- サニタイズ -->
            <br>
            メールアドレス
            <!-- もし $_POST['email']が空でなければ、入力データを表示 -->
            <input type="email" name="email" value="<?php if (!empty($_POST['email'])) {
                                                        echo h($_POST['email']);
                                                    } ?>"> <!-- サニタイズ -->
            <br>
            <!-- btn_confirm(確認ページ)に入力データが送信されるようにする -->
            <input type="submit" name="btn_confirm" value="確認する">
            <!-- トークンを保持 -->
            <input type="hidden" name="csrf" value="<?php echo h($token) ?>">
        </form>
    <?php endif; ?>
</body>

</html>

最後に送信を完了後は、$pageFlag = 2;セクション内で sessionトークンが残ってしまっているために、 <?php unset($_SESSION['csrfToken']); ?>
を記述してsessionトークンを削除するようにする。
すると新たな新規入力する時にはまた別のランダムなトークンが発行されるようになる。

以上学習したことの振り返りをしてみました。
ご指摘があればよろしくお願いします。

早速、ありがたく、CSRF対策の件でご指摘を受けましたので自分なりに修正したいと思います。

form.php
    <?php if ($pageFlag === 1) : ?>
        <?php if ($_POST['csrf'] === $_SESSION['csrfToken']) : ?>
            <form method="POST" action="input.php">
                // 省略
            </form>
        <?php endif; ?>
    <?php endif; ?>

上記のコードでは...

ページ遷移を無視した投稿がすり抜ける可能性がある

フォームが送信された際にCSRFトークンのチェックが行われていますが、このチェックが行われる前に、ページフラグ($pageFlag)が1の場合のみフォームが表示されます。つまり、正しいCSRFトークンを持っていない場合でも、ページフラグが1になるとフォームが表示されてしまいます。

これにより、攻撃者がCSRFトークンを持っていない状態で直接フォームにアクセスすることができ、正当なユーザーがフォームを通してアクセスした際には、トークンが正しいかどうかを確認していないために攻撃が可能になってしまいます。

この問題を解決するために、CSRFトークンが正しく生成され、フォームの送信時にそのトークンが確認されるようにようにしなければならない。
そうすれば、ページ遷移を無視して直接POSTリクエストを送信する攻撃からの保護が提供されると思うのでそれを考えた上での修正をしてみる。

以下のように修正する

sample.php
// フォームの送信時にトークンの一致を確認する
<?php if (isset($_SESSION['csrfToken']) && $_POST['csrf'] === $_SESSION['csrfToken']) : ?>

修正版

form.php
<?php

// sessionを使えるようにする
session_start();

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

// トークンが発行されているかの確認
if (!empty($_POST)) {
    echo '<pre>';
    var_dump($_POST);
    echo '</pre>';
}

$pageFlag = 0; // 入力画面のフラグ(Topのフォーム画面)

// $_POST['btn_confirm']が空でなければ $psteFlag = 1 に切り替える
if (!empty($_POST['btn_confirm'])) {
    $pageFlag = 1;
}

// $_POST['btn_submit']が空でなければ $psteFlag = 2 に切り替える
if (!empty($_POST['btn_submit'])) {
    $pageFlag = 2;
}

// サニタイズ htmlspecialchars関数
function h($str)
{
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
?>
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <!-- フォームに入力された確認画面の作成 -->
    <?php if ($pageFlag === 1) : ?>
        <!-- フォームの送信時にトークンの一致を確認する -->
        <?php if (isset($_SESSION['csrfToken']) && $_POST['csrf'] === $_SESSION['csrfToken']) : ?> <!-- 修正 -->
            <form method="POST" action="form.php">
                氏名
                <?php echo h($_POST['name']); ?> <!-- サニタイズ -->
                <br>
                メールアドレス
                <?php echo h($_POST['email']); ?> <!-- サニタイズ -->
                <br>
                ホームページ
                <?php echo h($_POST['url']); ?> <!-- サニタイズ -->
                <br>
                性別
                <?php
                echo $_POST['gender'] === '0' ? '男性' : '女性';
                ?>
                <br>
                年齢
                <?php
                if ($_POST['age'] === '1') {
                    echo '〜19歳';
                }
                if ($_POST['age'] === '2') {
                    echo '20歳〜29歳';
                }
                if ($_POST['age'] === '3') {
                    echo '30歳〜39歳';
                }
                if ($_POST['age'] === '4') {
                    echo '40歳〜49歳';
                }
                if ($_POST['age'] === '5') {
                    echo '50歳〜59歳';
                }
                if ($_POST['age'] === '6') {
                    echo '60歳〜';
                }
                ?>
                <br>
                お問い合わせ内容
                <br>
                <?php echo nl2br(h($_POST['contact'])); ?> <!-- サニタイズ -->
                <br>
                <!-- name属性の 'back' は $pageFlagの定義がないので 初期設定の '$pageFlag = 0;' になる -->
                <input type="submit" name="back" value="戻る">
                <!-- 完了ページへデータを送信する為のボタン($pageFlag = 2) -->
                <input type="submit" name="btn_submit" value="送信する">
                <!-- 各入力データの保持 hiddenの使用 -->
                <input type="hidden" name="name" value="<?php echo h($_POST['name']); ?>"> <!-- サニタイズ -->
                <input type="hidden" name="email" value="<?php echo h($_POST['email']); ?>"> <!-- サニタイズ -->
                <input type="hidden" name="url" value="<?php echo h($_POST['url']); ?>"> <!-- サニタイズ -->
                <input type="hidden" name="gender" value="<?php echo h($_POST['gender']); ?>"> <!-- サニタイズ -->
                <input type="hidden" name="age" value="<?php echo h($_POST['age']); ?>"> <!-- サニタイズ -->
                <input type="hidden" name="contact" value="<?php echo h($_POST['contact']); ?>"> <!-- サニタイズ -->
                <!-- トークンの保持 -->
                <input type="hidden" name="csrf" value="<?php echo h($_POST['csrf']); ?>"> <!-- サニタイズ -->
            </form>
            <?php else : ?> <!-- 不正時の処理 -->
                <p>不正なアクセスです。</p>
        <?php endif; ?>
    <?php endif; ?>

    <!-- 確認画面から送信後に遷移する完了画面の作成 -->
    <?php if ($pageFlag === 2) : ?>
        <?php if ($_POST['csrf'] === $_SESSION['csrfToken']) : ?>
            送信が完了しました。
            <!-- sessionトークンの削除 -->
            <?php unset($_SESSION['csrfToken']);  ?>
            <?php else : ?>
                <p>不正なアクセスです。</p>
                <?php unset($_SESSION['csrfToken']);  ?>
        <?php endif; ?>
    <?php endif; ?>

    <!-- トークンが発行されていないときはトークンを発行する -->
    <?php if ($pageFlag === 0) : ?>
        <?php if (!isset($_SESSION['csrfToken'])) {
            $csrfToken = bin2hex(random_bytes(32));
            $_SESSION['csrfToken'] = $csrfToken;
        }
        $token = $_SESSION['csrfToken'];
        ?>
        <!-- フォーム入力画面を作成する(TOP) -->
        <form method="POST" action="form.php">
            氏名
            <!-- もし $_POST['name']が空でなければ、入力データを表示 -->
            <input type="text" name="name" value="<?php if (!empty($_POST['name'])) {
                                                        echo h($_POST['name']);
                                                    } ?>"> <!-- サニタイズ -->
            <br>
            メールアドレス
            <!-- もし $_POST['email']が空でなければ、入力データを表示 -->
            <input type="email" name="email" value="<?php if (!empty($_POST['email'])) {
                                                        echo h($_POST['email']);
                                                    } ?>"> <!-- サニタイズ -->
            <br>
            ホームページ
            <input type="url" name="url" value="<?php if (!empty($_POST['url'])) {
                                                    echo h($_POST['url']);
                                                } ?>">
            <br>
            性別
            <!-- !emptyをしようすると0でもtrueになってしまう為にissetを使用する -->
            <input type="radio" name="gender" value="0" <?php if (isset($_POST['gender']) && $_POST['gender'] === '0') {
                                                            echo 'checked';
                                                        } ?>>男性
            <input type="radio" name="gender" value="1" <?php if (isset($_POST['gender']) && $_POST['gender'] === '1') {
                                                            echo 'checked';
                                                        } ?>>女性
            <br>
            年齢
            <select name="age">
                <option value="">選択してください</option>
                <option value="1" <?php if (isset($_POST['age']) && $_POST['age'] === '1') {
                                        echo 'selected';
                                    } ?>>〜19歳</option>
                <option value="2" <?php if (isset($_POST['age']) && $_POST['age'] === '2') {
                                        echo 'selected';
                                    } ?>>20歳〜29歳</option>
                <option value="3" <?php if (isset($_POST['age']) && $_POST['age'] === '3') {
                                        echo 'selected';
                                    } ?>>30歳〜39歳</option>
                <option value="4" <?php if (isset($_POST['age']) && $_POST['age'] === '4') {
                                        echo 'selected';
                                    } ?>>40歳〜49歳</option>
                <option value="5" <?php if (isset($_POST['age']) && $_POST['age'] === '5') {
                                        echo 'selected';
                                    } ?>>50歳〜59歳</option>
                <option value="6" <?php if (isset($_POST['age']) && $_POST['age'] === '6') {
                                        echo 'selected';
                                    } ?>>60歳〜</option>
            </select>
            <br>
            お問い合わせ内容
            <textarea name="contact"><?php if (!empty($_POST['contact'])) {
                                            echo h($_POST['contact']);
                                        } ?></textarea>
            <br>
            <input type="checkbox" name="caution" value="1">注意事項にチェックする
            <br>
            <!-- btn_confirm(確認ページ)に入力データが送信されるようにする -->
            <input type="submit" name="btn_confirm" value="確認する">
            <!-- トークンを保持 -->
            <input type="hidden" name="csrf" value="<?php echo h($token) ?>">
        </form>
    <?php endif; ?>
</body>

</html>
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1