Broken Access Control in Laravel
How Broken Access Control Manifests in Laravel
Broken Access Control in Laravel applications typically exploits the framework's implicit route model binding and middleware system. A common attack pattern involves manipulating resource IDs in URLs to access records that don't belong to the authenticated user. For example, if a user can modify the ID parameter in /api/posts/1 to /api/posts/2, they might access another user's data.
Laravel's default behavior makes this particularly dangerous. When you define a route like Route::get('/posts/{post}', PostController@show), Laravel automatically resolves the Post model using the ID from the URL. Without proper authorization checks, any authenticated user can access any post by simply changing the ID in the request.
Another manifestation occurs with policy methods. Developers often forget to register policies or fail to check authorization in controller methods. The @can directive in Blade templates provides frontend protection, but if your API endpoints don't verify permissions server-side, attackers can bypass client-side restrictions entirely.
Mass assignment vulnerabilities also contribute to broken access control. If you use $request->all() to create or update models without proper field filtering, attackers can modify fields they shouldn't have access to, such as user_id or role columns. Laravel's fillable property helps, but forgetting to set it on models creates a significant security gap.
Middleware order matters significantly. If your authentication middleware runs after your authorization middleware, the system might attempt to check permissions before knowing which user is making the request. This timing issue can cause authorization checks to fail or behave unpredictably.
API routes often suffer from missing authorization because developers assume authentication alone is sufficient. A typical vulnerable pattern looks like this:
public function update(Request $request, Post $post)
{
$post->update($request->all());
return response()->json($post);
}This code trusts that the authenticated user should be able to update any post, which is rarely the correct business logic.
Laravel-Specific Detection
Detecting broken access control in Laravel requires examining both code patterns and runtime behavior. Start by reviewing your routes files for resource routes that lack corresponding policy checks. Use php artisan route:list to identify all available endpoints, then trace each one back to its controller method.
Policy analysis is critical. Run php artisan policy:check to verify that all models have associated policies, though this command only checks existence, not implementation quality. Examine each policy method to ensure it properly validates user ownership or permissions. A common anti-pattern is returning true for all operations or checking only user_id equality without considering role-based permissions.
Middleware inspection reveals another layer of vulnerability. Check your app/Http/Middleware directory for custom authorization middleware that might have logic errors. The default Laravel middleware stack should include Illuminate\Foundation\Http\Middleware\Authorize::class, but developers sometimes remove or bypass it.
middleBrick's Laravel-specific scanning identifies broken access control by analyzing your API's runtime behavior. The scanner tests authenticated endpoints with different user contexts to detect whether users can access resources they shouldn't own. It examines your OpenAPI spec if available, then attempts to access endpoints with varying authorization headers to identify privilege escalation opportunities.
The scanner's BOLA (Broken Object Level Authorization) checks specifically target Laravel's route model binding vulnerabilities. It submits requests with manipulated IDs and analyzes the responses to determine if access control is properly enforced. For API endpoints, middleBrick tests whether authenticated users can access other users' resources by systematically varying identifier parameters.
middleBrick also checks for missing policy registrations. If your controllers use $this->authorize() or @can directives but lack corresponding policy classes, the scanner flags these as potential authorization gaps. The tool examines your auth configuration to ensure gates and policies are properly registered in the AuthServiceProvider.
Real-world scanning with middleBrick reveals patterns like exposed user IDs in URLs (/api/users/123/profile) without proper authorization checks, or API endpoints that return different error messages for valid vs invalid resource IDs, which can help attackers enumerate valid identifiers.
Laravel-Specific Remediation
Laravel provides several native mechanisms to fix broken access control. The most robust approach combines policies, middleware, and proper route protection. Start by generating policies for all models that contain user-specific data: php artisan make:policy PostPolicy --model=Post.
Implement comprehensive policy methods that cover all CRUD operations. A secure PostPolicy might look like:
class PostPolicy
{
public function viewAny(User $user)
{
return $user->hasPermission('view_posts');
}
public function view(User $user, Post $post)
{
return $user->id === $post->user_id || $user->hasPermission('view_all_posts');
}
public function update(User $user, Post $post)
{
return $user->id === $post->user_id;
}
public function delete(User $user, Post $post)
{
return $user->id === $post->user_id;
}
}Register your policies in AuthServiceProvider:
protected $policies = [
Post::class => PostPolicy::class,
User::class => UserPolicy::class,
// Add all models with authorization requirements
];Update your controllers to use authorization checks consistently:
public function show(Post $post)
{
$this->authorize('view', $post);
return response()->json($post);
}
public function update(Request $request, Post $post)
{
$this->authorize('update', $post);
$post->update($request->validated());
return response()->json($post);
}For API endpoints, use middleware to enforce authorization at the route level:
Route::apiResource('posts', PostController::class)
->middleware('can:view,post');
// Or use a custom middleware for complex authorization
Route::middleware(['auth:api', 'authorize.post'])->group(function () {
Route::get('/posts/{post}', [PostController::class, 'show']);
});Implement proper field filtering to prevent mass assignment vulnerabilities:
class PostController extends Controller
{
protected $fillable = ['title', 'content', 'published_at'];
public function update(Request $request, Post $post)
{
$this->authorize('update', $post);
$validated = $request->validate([
'title' => 'sometimes|string|max:255',
'content' => 'sometimes|string',
'published_at' => 'sometimes|date',
]);
$post->update($validated);
return response()->json($post);
}
}Use Laravel's built-in authorization features like the @can Blade directive for frontend protection, but always duplicate critical authorization checks server-side since client-side checks can be bypassed.
For complex authorization scenarios, implement gates in AuthServiceProvider:
Gate::define('update-post', function (User $user, Post $post) {
return $user->id === $post->user_id || $user->role === 'admin';
});Consider using Laravel's built-in authorization response methods to provide consistent error handling:
public function destroy(Post $post)
{
$this->authorize('delete', $post);
if (!$post->delete()) {
return response()->json([
'message' => 'Failed to delete post'
], 500);
}
return response()->json(null, 204);
}Finally, implement comprehensive logging for authorization failures to detect potential attack patterns. Laravel's built-in logging can capture failed authorization attempts when you wrap checks in try-catch blocks and log the exceptions.