technicalsuwako.moe/gemini/blog/fix-broken-contact-form.gmi

168 行
5.1 KiB
Plaintext
Raw Blame 履歴

このファイルには曖昧(ambiguous)なUnicode文字が含まれています

このファイルには、他の文字と見間違える可能性があるUnicode文字が含まれています。 それが意図的なものと考えられる場合は、この警告を無視して構いません。 それらの文字を表示するにはエスケープボタンを使用します。

=> /blog.gmi ブログ一覧へ
# 【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
<?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
ねぇねぇー!
簡単でしょー!
以上