In Depth: Common Authorisation Failures!

[In Depth #34] Let's explore a number of common ways developers fail authorisation in Laravel apps, and what you need to watch out for so you don't make the same mistakes!

In Depth: Common Authorisation Failures!
💡
We're digging into the weaknesses identified in the updated Laravel Security Audits Top 10 list for 2024. This week we're looking at #5 Missing Authorisation!

Last month we looked at Five Ways to Fail at Authentication, so I thought it would be fun to follow that up by covering a number of ways I've seen apps fail at Authorisation!

💡
As a quick refresher:
Authentication → who the user us.
Authorisation → what the user is allowed to do.

Missing Authorisation is incredibly common across all web apps, not just Laravel apps. In fact, it's so common that it currently sits in spot #1 on the OWASP Top 10: A01:2021-Broken Access Control. Although given it's sitting down at #5 in my own Laravel-specific list, maybe we're doing better than the rest? Laravel definitely makes it easier to implement authorisation than some other frameworks I've come across.

Let's start with the most common authorisation failure I see:

Insecure Direct Object References (IDOR)

For those who've never heard of them before, the most common form of Insecure Direct Object Reference vulnerability is when a URL contains a direct reference to a model or resource, and doesn't check if the user is actually allowed to access it.

Consider the following routes:

Route::get('/quests', [QuestController::class, 'index']);
Route::get('/quests/{quest}', [QuestController::class, 'show']);
Route::get('/quests/{quest}/edit', [QuestController::class, 'edit']);
Route::patch('/quests/{quest}', [QuestController::class, 'update']);
Route::delete('/quests/{quest}', [QuestController::class, 'destroy']);

Looks pretty typical so far, right?

Let's look at the controller next:

class QuestController extends Controller
{
    public function index(Request $request)
    {
        return view('quests.index', [
            'quests' => $request->user()->quests(),
        ]);
    }

    public function show(Quest $quest)
    {
        return view('quests.show', [
            'quest' => $quest,
        ]);
    }

    public function edit(Quest $quest)
    {
        return view('quests.edit', [
            'quest' => $quest,
        ]);
    }

    public function update(Request $request, Quest $quest)
    {
        $validated = $request->validate([
            'message' => 'required|string|max:255',
        ]);

        $quest->update($validated);

        return redirect(route('quests.index'));
    }

    public function destroy(Quest $quest)
    {
        $quest->delete();

        return redirect(route('quests.index'));
    }
}

Looks like a pretty typical web app, viewing, showing, editing, and deleting records. But there is something wrong... did you notice it?