0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

type="number" なのに文字列が来る? PHPバリデーションの落とし穴と ctype_digit で乗り越えた話

0
Posted at

はじめに

PHPでレビュー投稿フォームを実装していたとき、評価スコア(1〜5の整数)のバリデーションに思いがけず苦労しました。

type="number" で送ったんだから、数値として受け取れるでしょ」と思いきや、PHPはそう甘くない。この記事では、その落とし穴と、安全なバリデーションを実現するまでの思考プロセスを共有します。


フォームのHTML

<label for="score">評価(5点満点の整数)</label>
<input type="number" id="score" name="score" min="1" max="5">

一見シンプルなフォームです。type="number"minmax も指定しているし、「1〜5の整数しか送れないでしょ」と思うかもしれません。

しかし、この認識には 2つの落とし穴 があります。


落とし穴① $_POST の値はすべて文字列になる

HTMLの type="number" はあくまでブラウザのUI上の制御であり、PHPの$_POST(スーパーグローバル変数)で受け取った値は すべて文字列型(string) として扱われます。

// 実際に受け取る値のイメージ
$_POST['score'] = "3";  // 数値の 3 ではなく、文字列の "3"

なので、以下のような判定は危険です。

// ❌ 危険な例
if ((int)$_POST['score'] >= 1 && (int)$_POST['score'] <= 5) {
    // OK扱いにしてしまう
}

intによる型キャストは "3abc" のような文字列でも 3 に変換してしまいます。つまり 不正な入力をすり抜けさせてしまう 可能性があります。


落とし穴② minmax はブラウザでしか効かない

<input type="number" min="1" max="5"> の属性は、ブラウザ側のUI制限に過ぎません。

開発者ツールでHTMLを書き換えれば簡単に突破できます。

バリデーションは 必ずサーバーサイド(PHP側)でも行う 必要があります。


安全なバリデーションの実装

これらの落とし穴を踏まえて実装したコードが以下です。

if (!strlen($review['score'])) {
    $errors['score'] = '評価を入力してください';
} elseif (!ctype_digit($review['score'])) {
    $errors['score'] = '評価は数値で入力してください';
} else {
    $score = (int)$review['score'];
    if ($score < 1 || $score > 5)
        $errors['score'] = '評価は1~5の数値で入力してください';
}

3段階のチェックになっています。順番に解説します。


Step 1: 空文字チェック

if (!strlen($review['score'])) {
    $errors['score'] = '評価を入力してください';
}

strlen() で文字列の長さを取得し、0(空文字)であればエラーにします。

未入力の場合は $_POST['score'] が空文字 "" になるため、最初にこれを弾いておきます。


Step 2: ctype_digit() で「全文字が数字か」を確認

} elseif (!ctype_digit($review['score'])) {
    $errors['score'] = '評価は数値で入力してください';
}

ここが今回のキモです。

ctype_digit() は、文字列に含まれるすべての文字が十進数の数字(09)であるかを確認する関数です。

入力 ctype_digit() の結果
"3" true
"3abc" false
"3.0" false ❌(. が含まれるため)
"-1" false ❌(- が含まれるため)
"" false

(int) キャストと違い、"3abc" のような曖昧な文字列をきちんと弾いてくれるのが強みです。

この実装のヒントになったのが、uzullaさんの以下の記事です。

記事の中で ctype_digitis_numeric の使い分けが丁寧に解説されており、「シンプルで読みやすく、かつ高速に判定できる」方法として紹介されています。


Step 3: 型キャストして範囲チェック

} else {
    $score = (int)$review['score'];
    if ($score < 1 || $score > 5)
        $errors['score'] = '評価は1~5の数値で入力してください';
}

Step 2 を通過した時点で、値は「数字のみで構成された文字列」であることが保証されています。

ここで型キャストを使って整数に変換し、1〜5の範囲内かどうかを確認します。

Step 2 のフィルタリングがあるからこそ、このキャストが安全に使えます。


なぜ (int) だけじゃダメなのか、改めて整理

// ❌ これだけでは不十分
$score = (int)$_POST['score'];
if ($score < 1 || $score > 5) {
    // エラー
}

たとえば "3abc" という入力が来たとき:

  • (int)"3abc"3(PHPは先頭から数値として読める部分だけ変換する)
  • 結果、31〜5 の範囲内なのでエラーにならない ← バグ!

ctype_digit() を先に通すことで、このすり抜けを防げます。


まとめ

チェック 目的 使用関数
空文字チェック 未入力を弾く strlen()
文字種チェック 数字以外(記号・アルファベット・小数点等)を弾く ctype_digit()
範囲チェック 数値として1〜5の範囲かを確認する (int) キャスト

ポイントをまとめると:

  • type="number" は見た目の制御に過ぎない。サーバー側で必ずバリデーションを行う
  • $_POST の値はすべて文字列。(int) キャストだけでは不正値をすり抜けさせてしまう
  • ctype_digit() で「数字のみで構成されているか」を先にチェックすることで、安全な整数バリデーションが実現できる

おわりに

現在、エンジニアへの転職を目指して勉強中です。バリデーション一つでこんなに奥が深いとは思っておらず、実装を通じて改めてサーバーサイドのセキュリティ意識の大切さを学びました。

同じところで詰まっている方の参考になれば嬉しいです!

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?