Part 1 of series Backend Shorts
Whenever you are dealing with untrusted data, such as input from a HTTP request, you must validate it thoroughly in the backend of your web application.
function store(Request $request)
{
$validated = $this->validate($request, [
'name' => 'required|string|max:255',
'unit_price' => 'required|int|min:0|max:10000',
'currency' => 'required|in:USD,EUR',
'stock' => 'required|int|min:0|max: 1000000',
]);
$product = new Product($validated);
$product->save();
return redirect()->route('products.show', ['product' => $product->id]);
}
One of the easiest and safest ways to read request data in Laravel is the $this->validate
function inside a controller. It returns an array that contains the valid data (and only the valid data), or it throws a ValidationException
if the data is invalid.
You should generally avoid reading data directly from the request object with $request->get('field')
or similar methods, because it is very easy to forget to add the necessary validation. Reading data directly from the request object is a code smell.
Takeaways
- When it comes to user input, be paranoid.
- Always validate in the backend. Frontend validation offers no protection (although it is good for the UX).
- Avoid reading unvalidated data directly from the request.
Bonus
Sometimes untrusted input data flies under the radar because it is not immediately accessible. A common example for this would be a CSV import where you first need to parse the CSV content in order to get to the fields. Nevertheless, it is vital to validate the parsed data.
public function importStore(Request $request)
{
// Validate the file metadata.
$file = $this->validate($request, [
'file' => 'required|file|mimes:txt,csv|max:5120',
])['file'];
// Load CSV.
$csv = Reader::createFromPath($file->path());
$csv->setHeaderOffset(0);
$header = $csv->getHeader();
$input = iterator_to_array($csv->getRecords(), false);
// Set a limit for the maximum number of products per import.
if (count($input) > 1000) {
throw ValidationException::withMessages([
'file' => 'The import is limited to 1000 rows.',
]);
}
// Do a quick check to see if the header is correct. Although the
// validation logic further below would also find the error, it
// would produce multiple error messages for each line in the file.
if ($header !== ['name', 'unit_price', 'currency', 'stock']) {
throw ValidationException::withMessages([
'file' => 'The CSV file does not have the right header.',
]);
}
// Validate CSV file content.
$validated = Validator::make($input, [
'*' => 'required|array',
'*.name' => 'required|string|max:255',
'*.unit_price' => 'required|int|min:0|max:10000',
'*.currency' => 'required|in:USD,EUR',
'*.stock' => 'required|int|min:0|max: 1000000',
])->validate();
$instant = now();
foreach ($validated as &$entry) {
$entry['created_at'] = $instant;
$entry['updated_at'] = $instant;
}
Product::insert($validated);
return redirect()->route('products.index');
}
Series: Backend Shorts
- Part 1 – Validate all your inputs
- Part 2 – Use database transactions
One thought on “Backend shorts – Validate all your inputs”