Security: Hiding Menu Items with shouldRegisterNavigation() is Not Enough

2024-09-23

If you want to hide a navigation menu item for a certain role or condition, one way is to use the function shouldRegisterNavigation(). The problem is that hiding the menu item doesn't protect you from someone accessing that URL by typing it manually in the browser. Security issue!

Here's an example. Let's say we have a menu item, "User Approval", that should be accessible only for Admin users.

So, we put the condition for the role into the function shouldRegisterNavigation()

app/Filament/Resources/UserApprovalResource.php:

// ...
 
use App\Models\Role;
 
class UserApprovalsResource extends Resource
{
public static function shouldRegisterNavigation(): bool
{
return auth()->user()->role_id === Role::ADMIN;
}

This will hide the "User Resource" menu from anyone except the Admin. Great?

Not so fast. Look what happens when I register with a non-admin user and type the URL into the browser:

How do we protect from it? Two options.


Solution 1: Method canAccess()

You can define the same condition in the same Resource as a method canAccess():

app/Filament/Resources/UserApprovalResource.php:

class UserApprovalsResource extends Resource
{
public static function shouldRegisterNavigation(): bool
{
return auth()->user()->role_id === Role::ADMIN;
}
 
public static function canAccess(): bool
{
return auth()->user()->role_id === Role::ADMIN;
}

Then, if I type in the URL as a non-admin user, I will get the "403 | Forbidden" page.


Solution 2: Policies

Laravel Policies represent a more general solution for roles/permissions in Filament.

So, if you want to hide the menu item with all of its features, do this.

php artisan make:policy UserPolicy --model=User

This will generate a Policy file with many methods inside, but you can delete all of them and leave only viewAny().

Use the same condition as above, except that you don't need the auth()->user() anymore, as the Policy method automatically contains the $user.

app/Policies/UserPolicy.php:

namespace App\Policies;
 
use App\Models\Role;
use App\Models\User;
 
class UserPolicy
{
public function viewAny(User $user): bool
{
return $user->role_id === Role::ADMIN;
}
}

If you have that Policy in place, you don't need either of the methods shouldRegisterNavigation() or canAccess() in the Resource. Policies will automatically handle both in the background.

This tutorial's code comes from our Project Example Repeater: Choose Tournament Winners

A few of our Premium Examples: