Security Tip: Parameterise your Parameter Names!

[Tip #91] aka yet another example for why you should Never Trust User Input!

Security Tip: Parameterise your Parameter Names!

Today's Security Tip comes from a newly reported vulnerability I saw this morning on Twitter:

The primary protection we have against SQL Injection (SQLi) is to parameterise user input, to safely inject it into our SQL queries. It's standard practice, something we talk about all the time, and Laravel makes it pretty easy. In fact, I don't see many SQLi vulnerabilities in Laravel apps because Eloquent is so powerful.

💡
In my experience, the more complicated the query and the more raw SQL you need to write, the higher chance of SQLi.

Parameterisation is pretty standard practice, and we're all used to writing our queries in Laravel like this:

$projects = Project::where('active', true)
    ->when($request->access_code, fn($query, $accessCode) => 
        $query->where('access_code', $accessCode))
    ->when($request->preview_code, fn($query, $previewCode) => 
        $query->orWhere('preview_code', $previewCode))
    ->where('created_at', '>', now()->subYear())
    ->get();

However, something that often gets overlooked is that we don't just need to escape our values... we also need to escape our parameter names too!

Consider this code:

$firstKey = collect($request->query())->keys()->first();
$firstValue = $request->query($firstKey);

$projects = Project::where('active', true)
    ->where($firstKey, $firstValue)
    ->where('created_at', '>', now()->subYear())
    ->get();

It looks like a simplification to avoid adding more nested when() calls, and would handle each of these URLs nicely:

https://example.com/projects?access_code=mellon
https://example.com/projects?preview_code=precious
https://example.com/projects?uuid=7da13f1a-b74c-4b3c-aae5-c311fd866894

But what if this was provided:

https://example.com/projects?id=1

The access_code, preview_code, and uuid, would be completely bypassed, allowing access to whatever the first project is. You could then enumerate through each ID...

This is because the Parameter Name comes from user input, and is being blindly trusted, and you should never trust user input.

When dealing with database names and table columns, my recommendation is to use a list of allowed names and only use values within list. These things are rarely, if ever, dynamic, and you should know the structure of your app.

If you do need to inject dynamic parameter names into queries, limit the character set to remove special characters, and strip out sensitive keywords, etc. You need to limit these values as much as possible, as this will introduce risks. (Don't say I didn't warn you!)



Found this security tip helpful? Don't forget to subscribe to receive new Security Tips each week, and upgrade to a premium subscription to receive monthly In Depth articles, or toss a coin in the tip jar.

You can follow Stephen on Twitter, Fediverse, LinkedIn, Pinkary, Bluesky, and Threads, and reach out if you're looking for a Laravel Security Audit and Penetration Test or a budget-friendly Security Review.

Finally, to learn more about security through a series of interactive challenges, check out Stephen's new course: Practical Laravel Security.