Backend shorts – Validate all your inputs

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.
An elephant stretching for some leaves on a tree
Photo by Filip Olsok from Pexels

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

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.