Input validation means checking that data is what you expect before you use it. It will not replace escaping or prepared statements, but it removes whole classes of problems and keeps your data clean.
What to check
| Check | Example |
|---|---|
| Presence | Required fields are not empty |
| Type | A number really is a number |
| Length | Names are 1 to 100 characters |
| Range | Quantity is 1 to 99 |
| Format | Email, date, postcode, username pattern |
| Allowed values | Status is one of draft, published |
| Consistency | End date is after the start date |
Allow-list, not block-list
Decide what is allowed, and reject the rest. A block-list (“no <script>, no DROP“) always misses something.
<?php
function validateStatus(string $status): string
{
$allowed = ["draft", "published", "archived"];
if (!in_array($status, $allowed, true)) {
throw new InvalidArgumentException("Invalid status");
}
return $status;
}
echo validateStatus("published") . "\n";
try {
validateStatus("published' OR 1=1 --");
} catch (InvalidArgumentException $e) {
echo $e->getMessage();
}
published
Invalid status
Validate type, not just content
Users (and attackers) can send arrays where you expect strings.
<?php
function inputString(mixed $value, int $max = 255): ?string
{
if (!is_string($value)) {
return null;
}
$value = trim($value);
return ($value !== "" && mb_strlen($value) <= $max) ? $value : null;
}
var_dump(inputString(" Ana "));
var_dump(inputString(["x"])); // name[]=x
var_dump(inputString(str_repeat("a", 300)));
string(3) "Ana"
NULL
NULL
Numbers
<?php
function inputInt(mixed $value, int $min, int $max): ?int
{
$int = filter_var($value, FILTER_VALIDATE_INT, ["options" => ["min_range" => $min, "max_range" => $max]]);
return $int === false ? null : $int;
}
var_dump(inputInt("5", 1, 10));
var_dump(inputInt("5; DROP TABLE", 1, 10));
var_dump(inputInt("0", 0, 10));
int(5)
NULL
int(0)
Reject, do not “repair”
If the input is wrong, say so. Silently cleaning it (“removing bad characters”) can change the meaning, and attackers can exploit the cleaning code. Normal trimming and case folding are fine.
Validate on the server, always
JavaScript validation and the HTML required or pattern attributes are for user comfort. Anyone can skip them. The server’s check is the real one.
Validate early, near the edge
Convert raw request data into validated, typed values as soon as it arrives. The rest of the program then works with clean data (an int, a Status enum, a DateTimeImmutable) rather than strings.
<?php
enum Status: string
{
case Draft = "draft";
case Published = "published";
}
$input = ["status" => "published", "page" => "3"];
$status = Status::tryFrom((string) ($input["status"] ?? ""));
$page = max(1, (int) ($input["page"] ?? 1));
echo ($status?->name ?? "invalid") . ", page $page";
Published, page 3
Limits on size
Limit the length of every text field, the number of items in an array and the size of uploads and request bodies. Unlimited input is a denial-of-service risk.
Validation is not enough
A perfectly valid name can be <script>alert(1)</script>. Valid does not mean safe. You still need:
- Prepared statements for SQL.
- Output escaping for HTML.
Each protection has its own job.