jay-tank / idorguard
Static analyzer that flags Laravel controller actions receiving a route-model-bound Eloquent model with no visible authorization check in the method body.
Requires
- php: >=8.1
- nikic/php-parser: ^5.8
Requires (Dev)
- phpunit/phpunit: ^10.5 || ^11.0
This package is not auto-updated.
Last update: 2026-08-22 15:18:49 UTC
README
Static analyzer for a specific, well-documented Laravel footgun: route
model binding without an authorization check. Laravel resolves a
{invoice} URL segment straight into an Invoice $invoice object before
your controller method body even runs. It is trivial to write a controller
action that receives that model and returns/mutates it without ever
checking that the current user is allowed to touch that particular
record — an IDOR (Insecure Direct Object Reference) that only surfaces when
someone changes the ID in the URL. Today this is caught by manual code
review or, more often, not at all until a security report comes in — there
is no dedicated static tool that checks for it; the closest existing
tooling (e.g. Laravel Ranger) does type resolution for route bindings, not
authorization coverage.
Quick start
composer require jay-tank/idorguard --dev vendor/bin/idorguard check ./app/Http/Controllers
Built on nikic/php-parser — the
same AST library used by PHPStan and Psalm.
Example
class InvoiceController extends Controller { public function show(Invoice $invoice) { return view('invoices.show', ['invoice' => $invoice]); } }
$ vendor/bin/idorguard check app/Http/Controllers
app/Http/Controllers/InvoiceController.php:3: IDG001 Method show() receives a route-model-bound parameter ($invoice) but has no visible authorization check (no ->authorize(), Gate::, ->can()/->cannot(), or abort_if()/abort_unless() call) in its body.
idorguard: 1 finding(s).
Add $this->authorize('view', $invoice); (or a Gate::/->can()/
abort_unless() check) and the finding disappears.
Exit code is 1 when findings exist, 0 otherwise — safe to drop straight
into CI.
Library API
use IdorGuard\Scanner; $findings = Scanner::scanSource($phpSourceCode, "InvoiceController.php"); $findings = Scanner::scanPath("./app/Http/Controllers");
v0.1 scope
- One rule: IDG001 — a public controller method (class name/parent
ending in
Controller) with a class-typed parameter that is not aRequest/Response/similar framework helper type (i.e. it looks like a route-model-bound Eloquent model), with no->authorize(),Gate::...,->can()/->cannot()/->allows()/->denies(), orabort_if()/abort_unless()call anywhere in the method body. - Presence-based, not control-flow-sensitive: any authorization-signal call anywhere in the method body suppresses the finding, even if it is on an unrelated branch — a deliberate false-negative-favoring choice to keep false positives near zero for v0.1.
- Not in scope yet: verifying the authorization check actually targets the
same model parameter, Form Request
authorize()methods, policy-class cross-referencing, non-Laravel frameworks.
See DETAILS.md for the full design, and docs/USAGE.md for the complete API/CLI reference.
Examples
See examples/ for a snippet with the bug and one handling it correctly.
License
MIT