Santaji GadeDevelopment, PHP15 hours ago14 Views

A practical guide to validating and sanitizing user input in PHP forms, covering filter_var, allowlisting, context aware escaping, and safe file uploads.
Table of Contents
ToggleEvery field on a form is a door, and a visitor typing something unexpected into it is far more common than most templates plan for. Validating and sanitizing user input in PHP forms is what keeps a stray character from becoming a database error or a security hole.
Two different jobs get bundled into the same sentence so often that they start to sound like one task, but they are not. Unvalidated input still sits near the top of the OWASP Top 10 list of real world web application risks.
Validation asks whether a piece of data is acceptable at all. An email field either has a properly formed address in it or it does not.
Sanitization changes the data itself, stripping or encoding characters so it becomes safe to store, display, or pass along somewhere else.
A form that only sanitizes without validating will happily accept garbage. A form that only validates without sanitizing can still store something dangerous if the check itself has a gap.
The OWASP Input Validation Cheat Sheet treats this same distinction as foundational, and most real world security issues in PHP applications trace back to skipping one half of it.
PHP's own filter_var() function covers most everyday field types without any extra library.
$email = filter_var($_POST['email'], FILTER_VALIDATE_EMAIL);
$age = filter_var($_POST['age'], FILTER_VALIDATE_INT, [
'options' => ['min_range' => 0, 'max_range' => 120]
]);
$website = filter_var($_POST['website'], FILTER_VALIDATE_URL);
if ($email === false || $age === false) {
exit('One or more fields failed validation.');
}
Checking against false strictly with === matters, since a valid age of zero would otherwise be treated as falsy and rejected by mistake. Email address format itself is defined by RFC 5322, which is stricter and stranger than most validation regex people write by hand ever fully capture.
| Field Type | Filter to Use | Common Mistake |
|---|---|---|
| Email address | FILTER_VALIDATE_EMAIL | Skipping the check because the browser already has type email |
| Whole number | FILTER_VALIDATE_INT with a range | Forgetting to set min_range and max_range |
| URL | FILTER_VALIDATE_URL | Accepting a javascript scheme as valid |
| Free text | htmlspecialchars on output, not input | Sanitizing at input time and losing the original data |
Repeating the same filter calls across every form handler in a project invites inconsistency. A small class centralizes the rules in one place. A larger project can reach for a dedicated package like respect/validation instead of writing every rule by hand.
final class FormValidator {
private array $errors = [];
public function email(string $field, $value): ?string {
$clean = filter_var($value, FILTER_VALIDATE_EMAIL);
if ($clean === false) {
$this->errors[$field] = 'Enter a valid email address.';
return null;
}
return $clean;
}
public function requiredText(string $field, $value, int $maxLength = 500): ?string {
$trimmed = trim((string) $value);
if ($trimmed === '' || mb_strlen($trimmed) > $maxLength) {
$this->errors[$field] = 'This field is required and must fit the length limit.';
return null;
}
return $trimmed;
}
public function hasErrors(): bool {
return !empty($this->errors);
}
}
mb_strlen instead of strlen counts multibyte characters correctly, which matters the moment a visitor writes their name in a script that is not plain ASCII.
A denylist tries to block every dangerous pattern it can think of. An allowlist only accepts patterns it already knows are safe, and anything else fails by default.
// allowlist: only letters, numbers, spaces, and basic punctuation survive
$clean_name = preg_replace('/[^\p{L}\p{N}\s.\'-]/u', '', $_POST['name']);
An allowlist approach fails safe. A denylist that misses one obscure encoding trick fails open, letting exactly the input it was meant to stop slip through. This principle applies just as directly to the SQL injection and cross site scripting classes covered in OWASP's XSS prevention guidance.
Sanitizing on the way in is only half the job. The same value needs different treatment depending on where it ends up on the way out.
// output into HTML
echo htmlspecialchars($comment, ENT_QUOTES, 'UTF-8');
// output into a SQL query, using a prepared statement instead of escaping
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute([$email]);
// output into a URL query string
$url = 'https://brandella.in/search?q=' . urlencode($search_term);
Using the wrong escaping function for the destination is a common gap, and it shows up more often in real codebases than most developers expect. htmlspecialchars does nothing to protect a raw SQL query, and a prepared statement does nothing to protect an HTML page.
A checkbox group or a repeatable field sends an array instead of a single value, and each entry inside it needs the same checks as any other field. Skipping this step is an easy mistake, since a single value field works fine in testing right up until someone submits a form built to send an array where a string was expected.
$tags = $_POST['tags'] ?? [];
if (!is_array($tags)) {
exit('Unexpected input format.');
}
$clean_tags = array_map(
fn($tag) => preg_replace('/[^\p{L}\p{N}\s-]/u', '', trim((string) $tag)),
$tags
);
$clean_tags = array_filter($clean_tags, fn($t) => $t !== '');
The is_array check protects against a crafted request that sends a plain string where the form normally sends an array, which would otherwise break every loop that follows.
A file upload field is user input too, and the file's declared name and type cannot be trusted any more than a text field can. The IANA media types registry is the authoritative list behind the MIME type strings this section checks against.
$file = $_FILES['attachment'];
$allowed_types = ['image/jpeg', 'image/png', 'application/pdf'];
$actual_type = mime_content_type($file['tmp_name']);
if (!in_array($actual_type, $allowed_types, true) || $file['size'] > 5 * 1024 * 1024) {
exit('File rejected.');
}
mime_content_type() checks the file's actual content, not the extension or the client supplied MIME type, both of which are trivial for a visitor to fake. OWASP's file upload guidance covers several additional checks worth adding once a project handles uploads at any real scale.
A validator that only gets tested with obviously valid and obviously invalid input misses the values in between that actually reveal bugs.
$edge_cases = [
'', // empty string
' ', // whitespace only
'0', // falsy but valid
str_repeat('a', 10000), // far past any reasonable length
'test@example.com<script>', // valid prefix, malicious suffix
];
Running the validator class against a fixed list like this before every deploy catches the exact class of bug that only shows up on unusual, real world input. Wrapping the same list inside a PHPUnit test suite turns a manual check into something that runs automatically on every commit.
Eli the Computer Guy walks through a working filter_var based form validation example that pairs well with the validator class built in this guide.
Video credit: Eli the Computer Guy.
Wrapping the same filters shown in the video inside a reusable class is the main difference between a one off script and a maintainable form handler.
Validation decides whether input is acceptable and rejects it if not. Sanitization changes the input itself, stripping or encoding parts of it so it becomes safe to use.
Because any field a visitor can type into can also receive something unexpected, whether by mistake or on purpose, and unchecked input is behind most common web application vulnerabilities.
No. A request sent directly to the server, bypassing the browser entirely, skips any client side check completely, so every rule still needs to run again in PHP.
Generally no. Store the validated original value and use prepared statements for the database and context specific escaping for output, rather than permanently altering the stored data.
Skip the required check but still run the format check whenever a value is present, so an optional email field still rejects a malformed address if one is entered.
One accepts or rejects, the other transforms the data itself.
Email, integers, and URLs rarely need a custom check written from scratch.
Blocking only known bad patterns misses whatever nobody thought of yet.
HTML, SQL, and URLs each need a different escaping approach.
The declared MIME type and extension can both be faked easily.
Testing beyond obviously valid and invalid input catches what matters.
Pair validating and sanitizing user input in PHP forms with a wider security minded workflow to keep every incoming request accounted for.









