Backend shorts - Validate all your inputs
Tags: webdev, backend, laravel, shorts
Published: 2021-08-14 20:34:49

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.

```php
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.

![An elephant stretching for some leaves on a tree](https://hbgl.dev/wp-content/uploads/2021/08/pexels-filip-olsok-4003462-1024x683.jpg)<figcaption>Photo by <strong><a href="https://www.pexels.com/@filip-olsok-261056?utm_content=attributionCopyText&amp;utm_medium=referral&amp;utm_source=pexels">Filip Olsok</a></strong> from <strong><a href="https://www.pexels.com/photo/elephant-on-green-grass-field-4003462/?utm_content=attributionCopyText&amp;utm_medium=referral&amp;utm_source=pexels">Pexels</a></strong></figcaption>

## 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.

```php
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');
}
```