security
User-Submitted Data
Every value received from a browser, API client, webhook, file, cookie, or header must be treated as untrusted. The application decides what shape is acceptable before using it.
What Matters
- Read request data through a clear boundary rather than reaching into globals throughout the codebase.
- Reject missing, malformed, oversized, or unexpected values early.
- Use allow-lists when the valid set is known, such as sort direction or account status.
- Keep original input separate from normalised values so behaviour remains auditable.
- Never trust hidden form fields, query parameters, or client-side validation as security controls.
Practical Example
<?php
declare(strict_types=1);
function positiveId(mixed $value): ?int
{
$id = filter_var($value, FILTER_VALIDATE_INT);
return is_int($id) && $id > 0 ? $id : null;
}
echo positiveId('42') ?? 'invalid';
echo PHP_EOL;
echo positiveId('../etc/passwd') ?? 'invalid';
echo PHP_EOL;
// Prints:
// 42
// invalid
In Application Work
Job-facing code often receives a valid-looking value with the wrong meaning: another user ID, an unsupported enum, or a string where a positive integer is required. Validate shape and then authorise the requested action.
Input Arrives From More Than Forms
Untrusted input includes more than $_POST. Treat all of these as boundary data:
query strings and route parameters
cookies and request headers
JSON request bodies
uploaded filenames and MIME claims
webhooks and third-party API responses
CSV imports and queue messages
environment values supplied during deployment
The source changes the parsing step, but not the principle. Convert external data into a small validated internal shape before business logic uses it.
Validate Structure Before Meaning
First check whether the value has the expected type and size. Then check whether it is allowed for the current operation.
An integer project ID may be structurally valid while still naming another user's project. An allow-listed sort direction may be safe for query construction while still being unsupported on a particular screen.
Avoid Scattered Reads From Globals
Reading $_GET, $_POST, and $_SERVER throughout a codebase makes validation inconsistent. Controllers or request objects should collect external values at the edge and pass validated values inward. This keeps core code testable and makes reviews easier.
What To Check
Before moving on, make sure you can:
- treat all client-controlled values as untrusted;
- validate input at a clear boundary;
- use allow-lists and size limits;
- separate validation from authorisation;
- identify non-form sources of untrusted input.
Practice
Practice: Validate Request Values
Create a helper that validates a product ID and sort direction from a request.
Requirements
- Accept only positive integer product IDs.
- Allow only
name,price, andcreated_atsort fields. - Return validation errors without using invalid values.
Show solution
The solution uses allow-lists and rejects invalid IDs before business logic runs.
<?php
declare(strict_types=1);
function requestFilters(mixed $productId, mixed $sort): array
{
$id = filter_var($productId, FILTER_VALIDATE_INT);
$allowedSorts = ['name', 'price', 'created_at'];
$errors = [];
if (!is_int($id) || $id <= 0) {
$errors[] = 'product ID must be a positive integer';
}
if (!is_string($sort) || !in_array($sort, $allowedSorts, true)) {
$errors[] = 'sort field is not allowed';
}
return $errors === []
? ['valid' => true, 'product_id' => $id, 'sort' => $sort]
: ['valid' => false, 'errors' => $errors];
}
echo requestFilters('42', 'price')['valid'] ? 'valid' : 'invalid';
echo PHP_EOL;
echo requestFilters('-2', 'password')['valid'] ? 'valid' : 'invalid';
echo PHP_EOL;
// Prints:
// valid
// invalid
The valid branch contains values safe to pass into later application logic. Authorisation is still a separate step.