Protect your application from oversized inputs, malformed data and unnecessary processing with practical PHP and Laravel examples.
What does input validation mean?
Input validation checks whether submitted data meets your application's requirements before the application saves it or performs expensive work.
For a document submission, ask:
- Is the title text, and is it short enough?
- Is the description within the permitted length?
- Are there too many tags?
- Is every tag valid?
- Is the upload an allowed file type and size?
These are application rules. Put Laravel rules in a Form Request under app/Http/Requests/ or use $request->validate() in a controller. They are separate from PHP and web-server configuration.
Why is validation important for security?
| Security role | Example | Benefit |
|---|---|---|
| Bound accepted input | Title limited to 200 characters | Limits downstream text handling and storage |
| Bound repeated work | At most 20 tags | Limits loops and related database operations |
| Enforce expected types | Title must be a string | Rejects an array submitted in place of text |
| Restrict uploads | PDF only, up to 10 MiB | Reduces accepted formats and file sizes |
| Control processing cost | Image dimensions limited | Reduces exposure to very large decoded images |
| Restrict nested structure | Traveller object allows only name and age | Rejects unexpected keys |
Validation helps protect availability and data quality. It does not replace authentication, authorization, escaped output, parameterized queries, malware scanning or PHP security updates.
When should validation run?
Validate every untrusted entry point: browser forms, mobile APIs, administrative forms, imports and external integrations. Apply suitable equivalent rules to jobs that receive untrusted data.
Run validation before database writes, email generation, image resizing, document conversion or other expensive processing. Browser attributes such as maxlength and accept improve usability but can be bypassed.
Laravel validation happens after the request has reached PHP. Apply request-body limits at the web server as well, because validation cannot undo memory already used while receiving or parsing a request.
How does Laravel rule syntax work?
'title' => ['required', 'string', 'max:200'],
Meaning: the title is mandatory, must be text and may contain at most 200 characters.
What does max measure?
| Input type | Syntax | Meaning |
|---|---|---|
| String | string,max:200 |
At most 200 characters |
| Array | array,max:20 |
At most 20 elements |
| File | file,max:10240 |
At most 10,240 Laravel file-size kilobytes: 10 MiB |
| Integer | integer,max:20 |
Numeric value no greater than 20 |
Specify the intended type alongside the size rule. String characters and storage bytes are different measurements; align validation with database and encoding requirements.
What does each rule do?
| Rule | Meaning | Purpose |
|---|---|---|
required |
Field must be present and non-empty | Reject missing essential input |
nullable |
Field may contain null | Support optional values |
string |
Value must be text | Reject unexpected structures |
array |
Value must be an array | Enforce collection structure |
max:200 |
Maximum depends on field type | Bound accepted size |
tags.* |
Validate every tag | Check each element, not only its parent |
file |
Successfully uploaded file | Reject non-upload values |
mimes:pdf |
Infer permitted type from content | Restrict accepted file formats |
dimensions:max_width=4000,max_height=4000 |
Bound image width and height | Restrict pixel dimensions |
bail |
Stop later rules for a field after failure | Avoid unnecessary checks on that field |
nullable does not disable other rules for non-null values. mimes:pdf does not require a .pdf filename and does not prove a PDF is harmless.
How do I validate a document form in Laravel?
Generate a request class:
php artisan make:request StoreDocumentRequest
File: app/Http/Requests/StoreDocumentRequest.php
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreDocumentRequest extends FormRequest
{
public function authorize(): bool
{
// Use a policy when access depends on a resource or role.
return $this->user() !== null;
}
public function rules(): array
{
return [
'title' => ['bail', 'required', 'string', 'max:200'],
'description' => ['bail', 'nullable', 'string', 'max:20000'],
'tags' => ['bail', 'nullable', 'array', 'max:20'],
'tags.*' => ['bail', 'required', 'string', 'max:50'],
'document' => [
'bail', 'required', 'file', 'mimes:pdf', 'max:10240',
],
];
}
}
Controller example:
use App\Http\Requests\StoreDocumentRequest;
public function store(StoreDocumentRequest $request)
{
$data = $request->validated();
// Perform authorized storage and explicit database writes here.
// Use a configured private disk for sensitive documents.
return response()->json([
'message' => 'Document accepted.',
'title' => $data['title'],
], 201);
}
Then what happens?
| Submission | Expected result |
|---|---|
| Title containing 200 characters | Passes the title length rule |
| Title containing 201 characters | Fails |
| Title supplied as an array | Fails |
| Null description | Passes |
| 21 tags | Fails |
| One tag containing 51 characters | Fails |
| Valid 8 MiB PDF | Passes these file checks |
| 11 MiB PDF | Fails if it reaches application validation |
| PHP script renamed invoice.pdf | Renaming alone cannot satisfy content-based type checking |
JSON requests expecting JSON normally receive HTTP 422 for validation failures. Browser forms normally redirect back with errors. Earlier server or PHP limits can reject an upload before these rules run.
Why must I limit both arrays and their elements?
'tags' => ['array', 'max:20'],
'tags.*' => ['required', 'string', 'max:50'],
The first line bounds the count. The second bounds every value. Twenty enormous strings are still expensive, and thousands of short strings still create excessive repeated work.
For nested structures, validate each level:
$validated = $request->validate([
'travellers' => ['bail', 'required', 'array', 'min:1', 'max:10'],
'travellers.*' => ['bail', 'required', 'array:name,age'],
'travellers.*.name' => ['bail', 'required', 'string', 'max:100'],
'travellers.*.age' => ['bail', 'required', 'integer', 'min:0', 'max:120'],
]);
The array:name,age rule rejects additional keys such as is_admin. Use explicit validated fields in subsequent writes.
How do I validate vehicle photographs?
$validated = $request->validate([
'photos' => ['bail', 'required', 'array', 'min:1', 'max:5'],
'photos.*' => [
'bail',
'required',
'file',
'image',
'mimes:jpg,jpeg,png,webp',
'max:5120',
'dimensions:max_width=4000,max_height=4000',
],
]);
This example accepts up to five photos, each at most 5 MiB and at most 4,000 pixels in either dimension.
Why is file size alone insufficient?
A compressed image may be small on disk but large after decoding. A 12,000 × 12,000 image contains 144 million pixels. At an illustrative four bytes per pixel, a single buffer requires about 549 MiB. Resizing may allocate additional buffers.
Dimension checks help constrain accepted images, but parsing itself still needs resources. Keep image libraries patched and bound processing memory, time and concurrency. For PDFs, add processor-specific page and time limits; for archives, control entry count and decompressed size.
How do I perform similar checks in plain PHP?
Validate strings and arrays
This example requires the mbstring extension:
<?php
function validFields(array $input): bool
{
$title = $input['title'] ?? null;
$description = $input['description'] ?? null;
$tags = $input['tags'] ?? null;
if (!is_string($title)) {
return false;
}
if (trim($title) === '' || mb_strlen($title, 'UTF-8') > 200) {
return false;
}
if ($description !== null) {
if (!is_string($description)) {
return false;
}
if (mb_strlen($description, 'UTF-8') > 20000) {
return false;
}
}
if ($tags !== null) {
if (!is_array($tags) || count($tags) > 20) {
return false;
}
foreach ($tags as $tag) {
if (!is_string($tag)) {
return false;
}
if (trim($tag) === '' || mb_strlen($tag, 'UTF-8') > 50) {
return false;
}
}
}
return true;
}
Validate a PDF upload
Use a multipart form with enctype="multipart/form-data".
<?php
function rejectUpload(string $message): never
{
http_response_code(422);
exit($message);
}
$file = $_FILES['document'] ?? null;
if (!is_array($file)) {
rejectUpload('Document is required.');
}
if (
!isset($file['error'], $file['tmp_name']) ||
!is_int($file['error']) ||
!is_string($file['tmp_name'])
) {
rejectUpload('Invalid upload structure.');
}
if ($file['error'] !== UPLOAD_ERR_OK) {
rejectUpload('Upload failed.');
}
$tmp = $file['tmp_name'];
if (!is_uploaded_file($tmp)) {
rejectUpload('Invalid uploaded file.');
}
$size = filesize($tmp);
if ($size === false || $size === 0 || $size > 10 * 1024 * 1024) {
rejectUpload('Document must be non-empty and no larger than 10 MiB.');
}
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($tmp);
if ($mime !== 'application/pdf') {
rejectUpload('Only PDF documents are accepted.');
}
// Continue with authorized private storage and required scanning.
This example uses PHP 8.1+ syntax and the Fileinfo extension. Never trust the browser-supplied $_FILES['document']['type'] or original filename as proof of content.
Which production rules are appropriate, and which are risky?
These are example budgets, not universal security defaults. Match them to legitimate usage, database schema and measured processing costs.
| Input or operation | Appropriate example | Risky approach | Security concern |
|---|---|---|---|
| Title | required, string, max:200 | required only | No type or length bound |
| Description | nullable, string, max:20000 | nullable only | Non-null data unconstrained |
| Tags | array, max:20 | array only | Unbounded item count |
| Tag values | tags.* with string and max:50 | Parent validation only | Large or malformed elements |
| file, mimes:pdf, max:10240 | Extension check only | Misleading filename | |
| Photos | Count, file size and dimensions | image rule only | Processing cost insufficiently bounded |
| Nested objects | Allowed keys and child rules | Arbitrary keys accepted | Unexpected fields reach later code |
| Client validation | Browser and server checks | Browser checks only | Direct requests bypass controls |
| Database writes | Explicit validated fields | Blind use of request->all() | Unvalidated data reaches persistence |
| File processing | Bounded workers and quotas | Unlimited conversion requests | CPU, memory or queue exhaustion |
How should Laravel, PHP and Apache limits work together?
For one PDF of up to 10 MiB:
| Layer | Example setting | Purpose |
|---|---|---|
| Laravel | max:10240 | Application file limit |
| PHP | upload_max_filesize = 10M | Per-file upload ceiling |
| PHP | post_max_size = 12M | Total POST budget with overhead |
| Apache | LimitRequestBody 12582912 | Total request-body ceiling of 12 MiB |
Apply settings to the configuration actually serving the website. A form permitting five 5 MiB photos needs a larger total request budget than this single-document example. Include all files, fields and multipart overhead.
Then how do I verify the protection?
- Submit valid data and confirm success.
- Test each string at its limit and one character beyond it.
- Submit arrays at the maximum count and one element beyond it.
- Submit an array where a string is expected.
- Submit oversized individual elements and unexpected nested keys.
- Test allowed, renamed and oversized files.
- Test oversized dimensions before enabling image transformations.
- Confirm oversized bodies are rejected by the web server, normally with HTTP 413.
- Confirm application validation failures return useful errors without exposing internals.
- Measure memory and processing time with realistic concurrent traffic.
Sources and further reading
- Original Debug School checklist
- Laravel 12 validation documentation — examples follow this version; match your installed version.
- PHP upload handling
- PHP upload error codes
Colorful architecture: layered input validation and controlled processing
The web server bounds incoming bytes. Laravel checks application requirements. Accepted data proceeds to authorized storage or controlled processing; rejected input stops at the relevant layer.
flowchart TD
A["Browser or mobile API"] --> B["Web server: body and connection limits"]
B --> C["PHP: upload and POST limits"]
C --> D["Authentication and authorization"]
D --> E{"Laravel input validation"}
E -->|"Text"| F["String type and length"]
E -->|"Collections"| G["Array count, keys and elements"]
E -->|"Uploads"| H["File type, size and dimensions"]
F --> I{"All applicable checks pass?"}
G --> I
H --> I
I -->|"No"| J["Reject with validation errors"]
I -->|"Yes"| K["Explicit validated fields"]
K --> L["Authorized database writes"]
K --> M["Private file storage"]
M --> N["Bounded scanning or conversion jobs"]
N --> O["Authorized download or processed output"]
classDef client fill:#DBEAFE,stroke:#2563EB,color:#172554;
classDef boundary fill:#FEF3C7,stroke:#D97706,color:#78350F;
classDef checks fill:#EDE9FE,stroke:#7C3AED,color:#3B0764;
classDef accepted fill:#DCFCE7,stroke:#16A34A,color:#14532D;
classDef rejected fill:#FEE2E2,stroke:#DC2626,color:#7F1D1D;
classDef processing fill:#CFFAFE,stroke:#0891B2,color:#164E63;
class A client;
class B,C,D boundary;
class E,F,G,H,I checks;
class K,L,M accepted;
class J rejected;
class N,O processing;
Diagram rendering requires a Markdown platform with Mermaid support.
Top comments (0)