💾 Archived View for technicalsuwako.moe › blog › fix-broken-contact-form.gmi captured on 2024-08-31 at 12:30:49. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2023-09-08)

-=-=-=-=-=-=-

ブログ一覧へ

【PHP】正しい連絡フォームの作り方(クライアント側をぜったいに信用するな!!)

公開日:2023-08-04

問題

現在の「モダン」ウェブ開発で、連絡フォームはJavascriptで制御されていますが、これは大きなリスクがあります。

その理由について、直ぐに説明します。

以下のスクショをご覧いただいたら、何が問題は何だと思いますか?

/static/fuanform1.png

正解は:送信ボタンは`<form>`タグの外にある事です。

これでは、Javascriptを無効にした場合、送信ボタンをクリックする事が出来ません。

このフォームを送信する為には、この送信ボタンをフォーム内に移動し、`type="button"`を`type="submit"`に変更する事で、Javascriptなしでもフォームを送信する事が可能になります。

そんな感じ:

/static/fuanform2.png

そうして、入力画面で「required=""」というパラメータがあり、これによりJavascriptが無効であってもフィールドが入力されているかどうかを確認できます。

例:

/static/fuanform3.png

しかし、このパラメータを削除すると、どのような事態が起こると思いますか?

正解はこちら:

/static/fuanform4.png

また、確認画面ではフォームが`<input type="hidden" />`タグを沢山含んでいます。

その中の「value=""」部分を変更する事が可能です。

これにより、MySQLインジェクションも可能となります。

解決策

上述の問題を解決する為には、サーバー側でのチェックが必要です。

勿論、クライアント側とサーバー側の両方でチェックを行う事も可能です。

例として、PHPの場合を紹介します(PHPを使用するフォームが多い為):

<?php
  session_name("formvals");
  session_start([
    "cookie_httponly" => true,
  ]);

  if (empty($_SESSION["csrf_token"])) $_SESSION["csrf_token"] = bin2hex(random_bytes(32));
  if (!isset($_SESSION["step"])) $_SESSION["step"] = 1;
  $errmes = [];
  $reqvals = [
    "name" => $_SESSION["name"] ?? "",
    "kana" => $_SESSION["kana"] ?? "",
  ];
  $optvals = [
    "url" => $_SESSION["url"] ?? "",
  ];

  if ($_SERVER["REQUEST_METHOD"] == "POST") {
    if (!hash_equals($_SESSION["csrf_token"], $_POST["csrf_token"])) {
      die("不正なCSRFトークン");
    }

    if ($_SESSION["step"] == 1) {
      foreach ($reqvals as $k => $v) {
        $_SESSION[$k] = filter_input(INPUT_POST, $k, FILTER_SANITIZE_STRING);
        if ($_SESSION[$k]) $reqvals[$k] = $_SESSION[$k];
        else $errmes[] = $k."をご入力下さい。";
      }

      foreach ($optvals as $k => $v) {
        $_SESSION[$k] = filter_input(INPUT_POST, $k, FILTER_SANITIZE_STRING);
        $optvals[$k] = $_SESSION[$k];
      }

      if (empty($errmes)) $_SESSION["step"] = 2;
    }
    else if ($_SESSION["step"] == 2) {
      $_SESSION["step"] = 1;
      session_destroy();
      header("Location: /success.html");
      die();
    }
  }
  else {
    $_SESSION["csrf_token"] = bin2hex(random_bytes(32));
    $_SESSION["step"] = 1;
  }
?>

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>連絡フォーム</title>
  </head>
  <body>
<?php
  if ($_SESSION["step"] == 1) {
    if (count($errmes) != 0) {
?>
    <ul style="font-width: bolder; color: #f00; list-style: none;">
<?php
      foreach ($errmes as $e) {
        echo "<li>".$e."</li>";
      }
?>
    </ul>
<?php
    }
?>
    <form method="POST" action="/contact.php">
      <input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
      <table>
        <tbody>
          <tr>
            <td>お名前 (必須):</td>
            <td><input placeholder="山田 太郎" required="" name="name" type="text" value="<?= $reqvals["name"] ?>" /></td>
          </tr>
          <tr>
            <td>お名前 (かな) (必須):</td>
            <td><input placeholder="やまだ たろう" required="" name="kana" type="text" value="<?= $reqvals["kana"] ?>" /></td>
          </tr>
          <tr>
            <td>御社又は関連サイトのURL:</td>
            <td><input placeholder="https://076.moe/" name="url" type="text" value="<?= $optvals["url"] ?>" /></td>
          </tr>
        </tbody>
      </table>
      <button>確認画面へ</button>
    </form>
<?php
  } else if ($_SESSION["step"] == 2) {
?>
    <form method="POST" action="/contact.php">
      <input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
      お名前 (必須): <?= $reqvals["name"] ?><br />
      お名前 (かな) (必須): <?= $reqvals["kana"] ?><br />
      御社又は関連サイトのURL: <?= $optvals["url"] ?><br /><br />
      <button>送信する</button>
    </form>
<?php
  } else {
?>
    <p>不明なエラー。</p>
<?php
  }
?>
  </body>
</html>

結果:

/static/anzenform1.png

/static/anzenform2.png

/static/anzenform3.png

/static/anzenform4.png

/static/anzenform5.png

/static/anzenform6.png

ねぇねぇー!

簡単でしょー!

以上