【JavaScriptバリデーションの罠】フロント側の入力チェックを突破してみる

章: 2章

技術タグ: DevTools JavaScript セキュリティ バリデーション フォーム

今回やること

前回は、HTMLでお問い合わせフォームを作り、JavaScriptで空欄チェックを行いました。

何も入力せずに送信すると、JavaScriptがエラーメッセージを表示してくれました。

一見すると、これでフォームを守れているように見えます。

しかし、ここに罠があります。

JavaScriptの入力チェックは便利
でも、JavaScriptはブラウザ側で動く
つまり、ユーザー側で見えるし、避けられる可能性がある

今回は、前回作ったJavaScriptの空欄チェックを、あえて突破してみます。

目的は、不正なことをするためではありません。

目的は、フロント側バリデーションだけでは守れない理由を体験することです。


今回のゴール

今回のゴールは、以下です。

  • JavaScriptバリデーションの役割を理解する
  • DevToolsでJavaScriptの処理が見えることを確認する
  • ConsoleからJavaScriptの処理を一部変えられることを体験する
  • フロント側チェックだけでは防御にならないことを理解する
  • サーバー側バリデーションが必要な理由を説明できるようになる

今回の最重要ポイントはこれです。

フロント側バリデーションは、ユーザー補助。
サーバー側バリデーションは、最終防衛ライン。

前回のフォームを確認する

前回のフォームでは、送信時にJavaScriptが動くようにしていました。

送信ボタンを押すと、submit イベントが発生し、以下の処理が実行されます。

contactForm.addEventListener('submit', handleContactFormSubmit);

この処理によって、フォーム送信時に handleContactFormSubmit() が呼ばれます。

さらに、その中で event.preventDefault() によって通常送信を止め、JavaScriptで入力チェックをしていました。

function handleContactFormSubmit(event) {
  event.preventDefault();

  resetErrors();

  const isValid = validateContactForm();

  if (!isValid) {
    formMessage.textContent = '入力内容を確認してください。';
    return;
  }

  formMessage.textContent = '入力チェックを通過しました。';
}

つまり、前回のフォームは次のような流れでした。

送信ボタンを押す
↓
JavaScriptが通常送信を止める
↓
空欄チェックをする
↓
エラーがあれば画面に表示する
↓
問題がなければ通過メッセージを表示する

バリデーションとは何か

バリデーションとは、入力内容がルールに合っているか確認することです。

たとえば、お問い合わせフォームなら、次のようなチェックがあります。

  • お名前が空欄ではないか
  • メールアドレスが空欄ではないか
  • お問い合わせ内容が空欄ではないか
  • メールアドレスの形式になっているか
  • 文字数が長すぎないか

バリデーション自体は、とても大事です。

ただし、どこでバリデーションするかが重要です。

場所役割
フロント側ユーザーにすぐエラーを伝える補助
サーバー側送られてきたデータを最終確認する防御

今回突破するのは、フロント側のJavaScriptバリデーションです。


DevToolsでscript.jsを見てみる

まず、DevToolsを開きます。

  • 右クリックして「検証」
  • または F12
  • または Ctrl + Shift + I

Sourcesタブ、またはNetworkタブから script.js を探します。

そこには、前回書いたJavaScriptのコードが見えるはずです。

ここで重要なのは、フォームのチェック処理がブラウザ側に読み込まれているということです。

つまり、ユーザーはJavaScriptの処理を確認できてしまいます。

JavaScriptで何をチェックしているか
どの関数が動いているか
どのidを見ているか
どんなエラーメッセージを出しているか

この時点で、フロント側チェックは「見える場所」にあることがわかります。


突破方法1:JavaScriptを無効化されたらどうなるか

まず考えたいのは、JavaScriptが動かない場合です。

ブラウザの設定や拡張機能、通信エラーなどによって、JavaScriptが想定通り動かない可能性があります。

JavaScriptが動かなければ、前回作った空欄チェックも動きません。

つまり、JavaScriptだけに入力チェックを任せていると、そもそもチェック処理が実行されない可能性があります。

今回のフォームは event.preventDefault() で送信を止めているため、実際の送信先はまだありません。

しかし、実務のフォームでPHPなどの送信先がある場合、JavaScriptを通らずにデータが送られる可能性を考える必要があります。

JavaScriptが動く前提
↓
便利だが、防御としては弱い

突破方法2:Consoleからチェック関数を書き換えてみる

次に、ConsoleからJavaScriptの関数を書き換えてみます。

今回の学習環境では、Consoleから関数を上書きできる場合があります。

たとえば、バリデーション関数を常に true を返すようにしてみます。

validateContactForm = function () {
  return true;
};

これは、入力チェックを常に通過扱いにする改ざん例です。

この状態で送信ボタンを押すと、空欄でも「入力チェックを通過しました。」のような表示になる可能性があります。

もちろん、これは自分の学習用ファイルでだけ試してください。

ここで理解したいのは、ブラウザ側のJavaScriptはユーザー側で操作される可能性があるということです。


突破方法3:イベントリスナーを外してみる

前回のフォームでは、submitイベントにJavaScriptの処理を登録していました。

contactForm.addEventListener('submit', handleContactFormSubmit);

Consoleから、このイベントリスナーを外してみます。

contactForm.removeEventListener('submit', handleContactFormSubmit);

この操作により、送信時に handleContactFormSubmit() が呼ばれなくなる可能性があります。

つまり、JavaScriptの空欄チェックそのものを通らない状態に近づきます。

今回のフォームは実際の送信先をまだ作っていないため、ここでは「チェック処理を外せる可能性がある」という確認が目的です。

実務では、こうした可能性を前提に、サーバー側でも必ず入力チェックを行います。


突破方法4:HTMLのrequiredだけでも守れない

HTMLには、required という便利な属性があります。

たとえば、以下のように書くと、ブラウザが空欄チェックをしてくれます。

<input class="contact-form__control" type="text" id="name" name="name" required />

これは便利です。

しかし、required もブラウザ側の仕組みです。

DevToolsのElementsタブから属性を削除されたり、別の方法でリクエストを送られたりする可能性があります。

つまり、required もユーザー補助としては有効ですが、最終防衛ラインにはできません。

今回のフォームでは、学習のために novalidate を付けてブラウザ標準チェックを無効化していました。

<form id="contact-form" class="contact-form" action="#" method="post" novalidate>

このように、ブラウザ側のチェックは制御できてしまうものだと考える必要があります。


フロント側バリデーションは無意味なのか

ここで誤解してはいけないのは、フロント側バリデーションが無意味という話ではないことです。

フロント側バリデーションは、とても便利です。

  • 入力ミスにすぐ気づける
  • 画面遷移せずにエラーを表示できる
  • ユーザーのストレスを減らせる
  • サーバーへ送る前に明らかなミスを減らせる

つまり、フロント側バリデーションはUX改善に向いています。

ただし、防御の最終地点ではありません。

フロント側バリデーション
↓
ユーザーに優しい入力補助

サーバー側バリデーション
↓
システムを守る最終確認

なぜサーバー側チェックが必要なのか

サーバー側チェックが必要な理由は、ユーザーから送られてくるデータを信用できないからです。

フォームは、ユーザーが自由にデータを送ってくる入口です。

画面上では入力欄が3つしかなくても、実際のリクエストでは想定外の値が送られてくる可能性があります。

  • 空欄のまま送られる
  • 異常に長い文字列が送られる
  • HTMLタグが送られる
  • JavaScriptコードのような文字列が送られる
  • 想定していない項目が送られる

だから、サーバー側では必ず受け取ったデータを確認します。

ブラウザ側でチェック済みだから大丈夫、とは考えません。


LG流:入口の番人と城門の番人を分ける

フロント側バリデーションとサーバー側バリデーションは、役割が違います。

役割担当目的
入口の案内係JavaScriptユーザーが入力ミスに気づきやすくする
城門の番人PHP送られてきたデータを最終確認する

JavaScriptは、ユーザーに優しくするための案内係です。

PHPは、システムを守るための城門の番人です。

案内係がいるからといって、城門の番人を消してはいけません。

JavaScriptでチェックした
↓
だからPHPではチェックしなくていい

これは危険

正しい考え方は、次の形です。

JavaScriptでもチェックする
↓
PHPでも必ずチェックする

次回以降のPHPチェックのイメージ

次回以降は、PHPでフォームデータを受け取ります。

その時は、サーバー側でも以下のようなチェックを行います。

<?php

$name = trim($_POST['name'] ?? '');
$email = trim($_POST['email'] ?? '');
$message = trim($_POST['message'] ?? '');

$errors = [];

if ($name === '') {
    $errors[] = 'お名前を入力してください。';
}

if ($email === '') {
    $errors[] = 'メールアドレスを入力してください。';
}

if ($message === '') {
    $errors[] = 'お問い合わせ内容を入力してください。';
}

if (!empty($errors)) {
    // エラーがあれば処理を止める
    exit('入力内容を確認してください。');
}

// ここまで来たら、最低限の入力チェックを通過
echo '送信を受け付けました。';

このように、PHP側でも $_POST の中身を確認します。

JavaScriptでチェックしていたとしても、PHP側で再確認します。

同じようなチェックを二重に書くのは面倒に見えます。

しかし、役割が違います。

JavaScriptのチェック:ユーザー補助
PHPのチェック:防御

この違いを理解しておくと、フォーム処理の設計がかなり見えやすくなります。


今回わかったこと

今回は、JavaScriptの空欄チェックをあえて突破する考え方を学びました。

学んだことは以下です。

  • JavaScriptのバリデーションはブラウザ側で動く
  • ブラウザ側の処理はユーザーに見える
  • Consoleから関数を書き換えられる場合がある
  • イベントリスナーを外される可能性がある
  • required属性もブラウザ側の補助である
  • フロント側バリデーションはUX改善には有効
  • しかし、防御の最終地点にはできない
  • サーバー側バリデーションが必ず必要

フォームは、ユーザーが自由にデータを送ってくる入口です。

だからこそ、ブラウザ側だけではなく、サーバー側でも必ずチェックします。


次回予告

次回は、PHPでフォームデータを受け取ります。

今回までのJavaScriptチェックを通過したかどうかに関係なく、サーバー側であるPHPが $_POST を使ってデータを受け取ります。

次回のテーマは、以下です。

【PHPフォーム入門】$_POSTでお問い合わせ内容を受け取って表示してみる

ここから、フォームは本格的にサーバー側へ進みます。

ユーザーが送ってきたデータを受け取る。そして、そのデータをそのまま表示すると何が起きるのか。

次の闇、XSSへの入口が見えてきます。