Server-Side Input Validation in PHP
Why Validate Input?
Section titled “Why Validate Input?”User input is the main entry point for attacks on your application. Validation ensures data is safe and meets your requirements before processing.
The Validation Procedure
Section titled “The Validation Procedure”Follow these steps for every input field:
Step 1: Retrieve the Input Safely
Section titled “Step 1: Retrieve the Input Safely”Use filter_input() to safely retrieve values.
// For form data (POST)$email = filter_input(INPUT_POST, 'email');
// For query string parameters (GET)$id = filter_input(INPUT_GET, 'id');
// With validation (retrieve + validate in one step)$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);Step 2: Trim Whitespace
Section titled “Step 2: Trim Whitespace”Remove leading/trailing spaces from string inputs.
$email = trim($email ?? '');$search = trim($search ?? '');Step 3: Check Required Fields
Section titled “Step 3: Check Required Fields”If the field is required, check that it’s not empty.
if (empty($email)) { $errors[] = 'Email is required';}Step 4: Validate Format/Type
Section titled “Step 4: Validate Format/Type”Check that the data matches the expected format. Common examples:
- Email addresses: must contain
@and valid domain - Phone numbers: digits only, specific length (e.g., 10 digits)
- Postal codes: format varies by country (e.g.,
A1A 1A1in Canada,12345in USA) - ISBN: 10 or 13 digits with optional hyphens
- URLs: must start with
http://orhttps:// - Dates: specific format like
YYYY-MM-DD - Credit cards: 16 digits, passes Luhn algorithm
- Usernames: letters and numbers only, no spaces
// Email formatif (!filter_var($email, FILTER_VALIDATE_EMAIL)) { $errors[] = 'Invalid email format';}
// Integerif (!filter_var($id, FILTER_VALIDATE_INT)) { $errors[] = 'ID must be a number';}Step 5: Validate Business Rules
Section titled “Step 5: Validate Business Rules”Check application-specific constraints. Common examples:
- Minimum/maximum length: username must be 3-20 characters
- Number ranges: age must be 18-120, quantity must be 1-99
- Allowed values: status must be “active”, “pending”, or “inactive”
- Uniqueness: email or username must not already exist in database
- Relationships: end date must be after start date
- Permissions: user must own the resource they’re editing
- Stock limits: order quantity cannot exceed available inventory
// Length checkif (strlen($username) < 3 || strlen($username) > 20) { $errors[] = 'Username must be 3-20 characters';}
// Range checkif ($age < 18 || $age > 120) { $errors[] = 'Age must be between 18 and 120';}
// Allowed values$allowed = ['red', 'blue', 'green'];if (!in_array($color, $allowed)) { $errors[] = 'Invalid color selection';}Step 6: Proceed or Show Errors
Section titled “Step 6: Proceed or Show Errors”if (empty($errors)) { // Data is valid - process it} else { // Show errors to user}Validating Form Data (POST)
Section titled “Validating Form Data (POST)”Apply the procedure to a registration form:
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Step 1 & 2: Retrieve and trim $username = trim(filter_input(INPUT_POST, 'username') ?? ''); $email = trim(filter_input(INPUT_POST, 'email') ?? ''); $password = filter_input(INPUT_POST, 'password') ?? '';
// Step 3: Required fields if (empty($username)) { $errors[] = 'Username is required'; } if (empty($email)) { $errors[] = 'Email is required'; } if (empty($password)) { $errors[] = 'Password is required'; }
// Step 4: Format validation if (!empty($email) && !filter_var($email, FILTER_VALIDATE_EMAIL)) { $errors[] = 'Invalid email format'; }
// Step 5: Business rules if (!empty($username) && strlen($username) < 3) { $errors[] = 'Username must be at least 3 characters'; } if (!empty($password) && strlen($password) < 8) { $errors[] = 'Password must be at least 8 characters'; }
// Step 6: Process or show errors if (empty($errors)) { // Save to database... }}Validating Query String Data (GET)
Section titled “Validating Query String Data (GET)”Apply the procedure to URL parameters like products.php?id=5&page=2&search=laptop:
$errors = [];
// Step 1: Retrieve with type validation$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT);$search = trim(filter_input(INPUT_GET, 'search') ?? '');
// Step 3 & 4: Validate required parametersif ($id === false || $id === null) { $errors[] = 'Invalid product ID';}
// Step 5: Business rulesif ($page !== null && $page < 1) { $page = 1; // Default to page 1}
if (strlen($search) > 100) { $errors[] = 'Search query too long';}
// Step 6: Process or show errorsif (empty($errors)) { $product = fetchProductById($id); if (!$product) { $errors[] = 'Product not found'; }}Common Validation Patterns
Section titled “Common Validation Patterns”Validating IDs (Database Lookups)
Section titled “Validating IDs (Database Lookups)”$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if (!$id || $id < 1) { die('Invalid ID');}Validating Pagination
Section titled “Validating Pagination”$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?? 1;$page = max(1, $page); // Ensure minimum of 1Validating Search Queries
Section titled “Validating Search Queries”$search = trim(filter_input(INPUT_GET, 'q') ?? '');$search = substr($search, 0, 100); // Limit lengthValidating Selection Lists
Section titled “Validating Selection Lists”$sort = filter_input(INPUT_GET, 'sort') ?? 'newest';$allowed_sorts = ['newest', 'oldest', 'price_asc', 'price_desc'];
if (!in_array($sort, $allowed_sorts)) { $sort = 'newest'; // Default to safe value}Quick Reference
Section titled “Quick Reference”| Function | Purpose | Example |
|---|---|---|
filter_input(INPUT_POST, 'name') | Safely get POST value | Returns value or null |
filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT) | Get and validate as integer | Returns int, false, or null |
filter_var($email, FILTER_VALIDATE_EMAIL) | Validate email format | Returns email or false |
empty($value) | Check if empty/null/false | Returns true or false |
trim($string) | Remove whitespace | " hello " → "hello" |
strlen($string) | Get string length | "hello" → 5 |
is_numeric($value) | Check if numeric | "123" → true |
in_array($value, $array) | Check if in allowed list | Returns true or false |
ctype_alnum($string) | Check letters/numbers only | "abc123" → true |
Common Mistakes
Section titled “Common Mistakes”- Skipping server-side validation - Client-side validation can be bypassed
- Using
$_GET/$_POSTdirectly - Usefilter_input()for safer retrieval - Not validating IDs - Always validate before database queries
- Vague error messages - Tell users what’s wrong so they can fix it
- Not preserving valid input - Keep valid fields filled when showing errors
- Forgetting edge cases - Test empty, very long, and special character inputs