Laravel Missing Authorization: How to Add Policy and Gate Checks to Controllers
Controllers with store, update, and destroy actions but no authorization let any authenticated user modify any record. Add policies to enforce ownership.
Authorization — determining what an authenticated user is allowed to do — is the second half of access control, and the half most frequently implemented incorrectly. Applications that verify a user is logged in (authentication) but do not verify that the logged-in user owns the requested resource (authorization) are vulnerable to Insecure Direct Object References (IDOR). An IDOR occurs when an attacker changes a resource identifier in a URL or request body and accesses another user's data.
IDOR attacks require no special tools. An attacker with a valid account changes GET /invoices/1001 to GET /invoices/1002 and views another customer's invoice. They iterate: 1003, 1004, 1005 — each returning a different user's confidential data. POST requests are equally vulnerable: PUT /profile/456 with attacker-supplied data modifies a different user's profile. Modern browsers, Burp Suite, and simple cURL scripts make this iteration trivial. The only defense is checking ownership server-side before returning or modifying any resource.
Broken Access Control was ranked the #1 vulnerability in the OWASP Top 10 for 2021. The ranking reflects how consistently authorization checks are missing or implemented incorrectly. Laravel provides an excellent policy and gate system for this problem, but it requires explicit use. No framework can automatically determine ownership relationships — you must define them in policies and call $this->authorize() in your controllers. An auth middleware only answers "is this user logged in?" — it never answers "can this user access this record?"
The Problem
Authentication verifies who a user is. Authorization verifies what they can do. A controller that checks authentication (auth middleware) but not authorization lets any logged-in user create, edit, or delete any record — including other users' data. This is called an Insecure Direct Object Reference (IDOR) and is consistently in the OWASP Top 10. If your PostController lets user A update user B's posts, you have a missing authorization vulnerability.
How to Fix
-
1
Create a policy for each model
Generate policies for models that need access control:php artisan make:policy PostPolicy --model=Post// app/Policies/PostPolicy.php public function update(User $user, Post $post): bool { return $user->id === $post->user_id; }public function delete(User $user, Post $post): bool { return $user->id === $post->user_id; }public function create(User $user): bool { return true; // Any authenticated user can create } -
2
Apply authorization in controllers
Use the authorize() method or Gate facade:
// Using authorize() — throws 403 if denied public function update(Request $request, Post $post) { $this->authorize('update', $post); // ... update logic }// Using middleware for all actions public function __construct() { $this->authorizeResource(Post::class, 'post'); }// Using Gate directly if (Gate::denies('update', $post)) { abort(403); } -
3
Scope queries to the authenticated user
In addition to policies, scope database queries to prevent data leakage:
// WRONG — returns any user's post $post = Post::findOrFail($id);// RIGHT — only returns the authenticated user's post $post = auth()->user()->posts()->findOrFail($id);// Or use a global scope class Post extends Model { protected static function booted() { static::addGlobalScope('owned', function (Builder $builder) { if (auth()->check()) { $builder->where('user_id', auth()->id()); } }); } }
How to Verify
Test by logging in as User A and trying to access User B's resources:
# Get User B's post ID, then try to update it as User A
curl -X PUT https://yourapp.com/api/posts/123 \
-H 'Authorization: Bearer USER_A_TOKEN' \
-d 'title=Hacked'
This should return 403 Forbidden, not 200 OK. Run php artisan stackshield:scan --check=SS053 to find controllers missing authorization.
Prevention
Use authorizeResource() in controller constructors for automatic policy mapping. Add authorization checks to your code review checklist. Write feature tests that verify User A cannot access User B's resources. Use StackShield to scan for controllers missing authorization.
Frequently Asked Questions
Is auth middleware enough for authorization?
No. Auth middleware only verifies the user is logged in. It does not check whether that specific user has permission to perform the requested action on the requested resource. You need both: auth middleware (who) and policies/gates (what).
Should I use policies or gates?
Use policies when authorization logic is tied to a specific model (PostPolicy for Post). Use gates for general abilities not tied to a model (Gate::define('manage-settings')). Most CRUD controllers should use policies.
What is an IDOR vulnerability and how does it relate to missing authorization?
An Insecure Direct Object Reference (IDOR) is the most common manifestation of missing authorization: a request contains a reference to a database record (like an ID in the URL), and the server returns the record without checking whether the requesting user owns or has permission to access it. Fixing IDOR means checking ownership before every database lookup — auth()->user()->posts()->findOrFail($id) instead of Post::findOrFail($id).
How do I write tests to verify authorization is working?
Create two test users and verify that user A cannot access user B's resources. A minimal test: create a post belonging to $userA, authenticate as $userB, attempt PUT /posts/{$userA->post->id}, and assert a 403 response. Repeat for show, edit, and destroy actions. These tests are fast, catch authorization regressions, and serve as living documentation of your access control model.
Can Laravel's route model binding handle authorization automatically?
Route model binding automatically resolves a model from the URL parameter, but it does not perform authorization. Route::get("/posts/{post}", ...) will inject any Post record, not just those belonging to the authenticated user. You still need to call $this->authorize('view', $post) or scope the binding with a custom resolveRouteBinding method on the model.
Related Security Terms
Related Guides
Laravel Mass Assignment Vulnerability: How to Protect Eloquent Models with $fillable and $guarded
Eloquent models without $fillable or $guarded allow attackers to set any database column through request input, including is_admin, role, or email_verified_at.
Fix Missing CSRF Protection in Laravel: @csrf, VerifyCsrfToken & API Routes
Laravel forms without @csrf tokens are vulnerable to cross-site request forgery. Learn how to add CSRF protection, configure VerifyCsrfToken exceptions, and handle CSRF for API routes.
Laravel Request Validation: How to Stop Using Raw $request Input in Controllers
Controllers using $request->input() or $request->all() without validation expose your app to type juggling, injection, and data corruption attacks.
Is your Laravel app exposed right now?
34% of Laravel apps we scan have at least one critical issue. Most teams don't find out until something breaks. Our free scan checks your live application in under 60 seconds.