Security Tip: Validating Array Inputs

[Tip#42] Validating single values is easy, but what about arrays?

Security Tip: Validating Array Inputs

Greetings friends! If you missed last week’s Tip on Safely Rendering JSON in Blade, definitely go back and check it out. Of particular note is the discussion in the comments about why I recommend avoiding the `@js` and `@json` Blade helpers, plus the differences between the different methods. If you’d like me to dig more into these topics, please let me know!

This week we’re starting a new mini-series, tying into the threads I’m building on Twitter1 and the Fediverse2, covering the Top 10 security issues and vulnerabilities I’ve discovered while auditing Laravel apps. We will work through the Top 10 by adding tips and maybe some In Depths, to cover any of the topics we haven’t covered yet.

In the #10 position on my list is Insufficient Input Validation. We’ve covered different aspects of validation a couple of times3, but there is still one aspect we need to talk about: Validating Array Inputs. So that’s what we’re covering today. 🤓

Please consider becoming a paid subscriber to support Laravel Security in Depth.
You’ll receive weekly security tips and monthly In Depth articles, covering every aspect of building secure applications in Laravel.

👉 Security Audits: Just how secure is your app? Book a security audit and get that sneaky overlooked vulnerability found and fixed! 🕵️

Looking to learn more?
Security Tip #27: Leaking Model Existence
▶️ In Depth #10: Magic Emails

Validating Array Inputs

A common excuse for not validating inputs is: complexity.

When building a complicated interface, it’s easy to get overwhelmed by the number of inputs your passing around, and then adding in arrays - and nested arrays - just bumps up the complexity significantly.

As a developer, I totally get it. I’ve done the same! But as a security person, and a hacker, I see opportunities to inject stuff! 😈

When attacking a form4, I specifically go looking for nested array inputs, and then see what I can inject into them. I look for sensitive columns, such as `users.admin`, and see what it’ll accept. Nested and related models are gold for this, as they are often blindly synced in batch, rather than carefully checked individually.

So how should you validate array inputs?

There are a few options, depending on what you need to do. Let’s take a look at each in turn.

  1. `array` validation rule5.
    It allows you to specify which fields must be present in the input array.

    $input = [
        'user' => [
            'name'  => 'Frodo Baggins',
            'sword' => 'Sting',
            'ring'  => true,
        ],
    ];
     
    $request->validate([
        'user' => 'array:name,sword,ring',
    ]);.
  2. Simple dot-notation for nested values.
    Like elsewhere in Laravel, you can use dot-notation to traverse arrays, and assign unique rules to each array item. This is a great approach for ensuring each value is properly validated.

    $input = [
        'user' => [
            'name'  => 'Frodo Baggins',
            'sword' => 'Sting',
            'ring'  => true,
        ],
    ];
     
    $request->validate([
        'user.name'  => 'required|string',
        'user.sword' => 'nullable|string',
        'user.ring'  => 'required|boolean',
    ]);.
  3. Wildcard dot-notation for nested arrays.

    When dealing with arrays of items nested under a key, you can use the `*` wildcard to easily validate the keys within the array items.

    $input = [
        'users' => [
            ['name' => 'Frodo Baggins', 'sword' => 'Sting', 'ring' => true],
            ['name' => 'Gollum', 'sword' => null, 'ring' => false],
        ],
    ];
     
    $request->validate([
        'users.*.name'  => 'required|string',
        'users.*.sword' => 'nullable|string',
        'users.*.ring'  => 'required|boolean',
    ]);.

A note about Laravel 8 and backwards compatibility…

In Laravel 8 and prior, nested array values were returned from the validator regardless of there being a matching validation rule, which differed from the behaviour of the root level values within the validator. This posed a mass-assignment security risk, allowing you to inject extra values into nested models, bypassing the validator’s protection.

The `Validator::excludeUnvalidatedArrayKeys()` method was introduced into Laravel 8 as an optional way to disable this risky functionality6. Laravel 9 swapped the behaviour, so by default the validator would only return validated keys across all levels, and `Validator::includeUnvalidatedArrayKeys();` was introduced to toggle the legacy behaviour.

With this in mind, if you’re still on Laravel 8, consider adding `Validator::excludeUnvalidatedArrayKeys()` to your app service provider. Note this has the potential to break your validators, so you need to carefully test it before toggling it on.

If you’ve upgraded from Laravel 8, check if you have `Validator::includeUnvalidatedArrayKeys();` enabled. If you do, it’s probably because something broke during the upgrade. Now is a great time to work out what broke, so you can add in those missing validation rules and disable it. 😉


  1. I tried to embed the Tweet, but apparently Substack is blocked from embedding??
    You can find the thread here:

    https://twitter.com/valorin/status/1642069402773504001

  2. You can find the start of the thread here: https://infosec.exchange/@valorin/110122481699661771

    I’m also posting on Post, Spoutible, and LinkedIn, if you’re on one of those too.

  3. We’ve covered these topics previously:

  4. During an audit, in a totally ethical manner…

  5. See: https://laravel.com/docs/10.x/validation#rule-array

  6. To avoid backwards compatibility issues, the fix was opt-in until the next major version - i.e. Laravel 9.