Prevent IDOR vulnerabilities in your Laravel apps by using attribute-based access control in Eloquent models. Secure your data with clean, automated logic.

During an on-call rotation last year, I spent about four hours tracing a data leak where a user could view another company's invoice simply by changing an integer in the URL. We had plenty of tests, but we’d relied on manual authorization checks in our controllers, and eventually, someone missed one during a refactor.
It’s a classic story: you trust your routes, but you forget that the database doesn't know who the current user is. To fix this, I stopped putting if ($user->id !== $invoice->user_id) inside my controllers and started enforcing access constraints directly at the model layer.
An Insecure Direct Object Reference (IDOR) happens when an application provides direct access to objects based on user-supplied input without proper validation. If your route is /invoices/{id}, your controller probably looks like this:
PHPpublic function show($id) { $invoice = Invoice::findOrFail($id); return view('invoices.show', compact('invoice')); }
The findOrFail method is a trap. It executes SELECT * FROM invoices WHERE id = ?. If the ID exists, the database returns it. It doesn't care if the logged-in user owns that invoice. You're effectively leaving the vault door open and asking the user to please not look inside.

We first tried adding global scopes to our models, but that broke our admin dashboard where moderators needed to see everything. Instead, I moved to an attribute-based approach that ties the authorization logic to the model's relationships.
If you are new to the basics of ORM usage, I recommend reviewing Eloquent basics: models, relationships, and your first queries to ensure your schema supports these checks.
The goal is to move authorization into the model's query scope so that the database cannot return an object unless the current user has permission to see it.
Instead of fetching by ID, we use a scoped query that injects the user's context. Here is how I structure this in my models:
PHP#6A9955">// Inside your Invoice model public function scopeForUser($query, $user) { return $query->where('user_id', $user->id); }
Now, your controller becomes much safer:
PHPpublic function show($id) { $invoice = Invoice::forUser(auth()->user())->findOrFail($id); return view('invoices.show', compact('invoice')); }
If the ID doesn't belong to the user, findOrFail throws a ModelNotFoundException, which Laravel automatically converts to a 404. The attacker doesn't even know the record exists, let alone that they shouldn't see it.
While scopes handle data retrieval, Mastering Laravel Policies: A Practical Guide to Authorization Logic is still necessary for actions like updating or deleting. However, combining Policies with attribute-based access control creates a "defense in depth" strategy.
I often use a Trait to enforce this across my codebase. By creating a BelongsToUser trait, I can ensure every model that needs this protection has a standard scopeForUser method, preventing me from forgetting it in a new controller.
I’ve found that enforcing these checks at the query level adds roughly 0.2ms to the database execution time—a cost I'm happy to pay. However, you have to be careful with complex multi-tenant applications. If you're building a SaaS, you might need to check for tenant_id instead of user_id.
If you're dealing with complex data transformations, you might be tempted to use accessors, but be careful. As I've noted in my guide on Laravel Eloquent Accessors and Mutators: A Practical Guide, those are for formatting, not for security. Never rely on an accessor to hide sensitive data; the data is already in memory by that point.
Q: Does this replace Laravel Policies? A: No. Scopes prevent the data from being retrieved in the first place (data privacy), while Policies handle the business logic of whether a user is allowed to perform an action (authorization).
Q: What if an admin needs to bypass these scopes?
A: You can add a check inside your scope: if ($user->isAdmin()) return $query;. Just be explicit and keep that logic centralized.
Q: Is this enough to stop all IDOR? A: It's a massive step forward, but you still need to ensure your API endpoints are protected by middleware and that your route parameters are properly validated.
I’m still experimenting with using custom collection classes to enforce these checks automatically upon retrieval, but for now, the scoped query approach is the most reliable pattern I’ve found. It’s readable, it’s testable, and most importantly, it makes it incredibly difficult for a developer to accidentally introduce an IDOR vulnerability during a late-night hotfix.
Don't rely on your memory to check permissions. Make the database do the heavy lifting for you.
Secure file uploads from the ground up require more than basic validation. Learn how to prevent RCE and directory traversal in your production systems.