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.

Maintainers

Package info

github.com/jay-tank/idorguard

pkg:composer/jay-tank/idorguard

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-08-20 14:01 UTC

This package is not auto-updated.

Last update: 2026-08-22 15:18:49 UTC


README

Packagist

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 a Request/Response/similar framework helper type (i.e. it looks like a route-model-bound Eloquent model), with no ->authorize(), Gate::..., ->can()/->cannot()/->allows()/->denies(), or abort_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