From 5c2e481985f4232d71452e343d98d2cd2ce47b64 Mon Sep 17 00:00:00 2001 From: nexxo Date: Sun, 26 Jul 2026 20:04:34 +0200 Subject: [PATCH] fix(security): console and account checks now survive a Livewire action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Livewire re-applies only the middleware on its persistent list when an action posts to /livewire/update. Its defaults cover auth; ours were not on the list — only RestrictAdminHost had been added. So EnsureAdmin and EnsureCustomerActive guarded the PAGE and not the actions, and the page is not where the actions run: a signed-in non-operator who could reach the admin host could drive console components, and a suspended customer could keep driving the portal. Found by Codex while reviewing the domain-separation design, and confirmed against Livewire's own default list in vendor. Co-Authored-By: Claude Opus 5 --- app/Providers/AppServiceProvider.php | 14 +++++++++++++- tests/Feature/Admin/RbacTest.php | 13 +++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index f73bc8b..4f34806 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -17,6 +17,8 @@ use App\Services\Traefik\SshTraefikWriter; use App\Services\Traefik\TraefikWriter; use App\Services\Wireguard\LocalWireguardHub; use App\Services\Wireguard\WireguardHub; +use App\Http\Middleware\EnsureAdmin; +use App\Http\Middleware\EnsureCustomerActive; use App\Http\Middleware\RestrictAdminHost; use App\Mail\MaintenanceCancelledMail; use App\Models\MaintenanceNotification; @@ -57,7 +59,17 @@ class AppServiceProvider extends ServiceProvider // path-based guard would skip. Marking the host restriction persistent // makes Livewire re-apply it from the component's original route, so an // admin action cannot be driven through a public hostname. - Livewire::addPersistentMiddleware(RestrictAdminHost::class); + // Livewire re-applies only the middleware on this list when an action + // posts to /livewire/update. Its own defaults cover `auth` — but not + // ours. Without these two, a signed-in NON-operator could drive console + // components, and a suspended customer could keep driving the portal: + // the page would never load for them, but the page is not where the + // actions run. + Livewire::addPersistentMiddleware([ + RestrictAdminHost::class, + EnsureAdmin::class, + EnsureCustomerActive::class, + ]); // Send-time guard for maintenance mail (X-CP-Notification carries the // ledger id). Deliberately READ-ONLY — it never marks the row sent or diff --git a/tests/Feature/Admin/RbacTest.php b/tests/Feature/Admin/RbacTest.php index 23d25f8..a2884e5 100644 --- a/tests/Feature/Admin/RbacTest.php +++ b/tests/Feature/Admin/RbacTest.php @@ -44,3 +44,16 @@ it('lets Support retry provisioning but not manage datacenters', function () { // ...but cannot manage datacenters. Livewire::actingAs(operator('Support'))->test(Datacenters::class)->call('toggle', 'x')->assertForbidden(); }); + +it('replays the operator and account checks on every Livewire action', function () { + // Livewire re-applies only the middleware on this list when an action posts + // to /livewire/update, and its defaults cover auth but not ours. Without + // these, the console page would refuse a non-operator while the console's + // actions still answered them — the page is not where the actions run. + $persistent = app(\Livewire\Mechanisms\PersistentMiddleware\PersistentMiddleware::class) + ->getPersistentMiddleware(); + + expect($persistent)->toContain(\App\Http\Middleware\EnsureAdmin::class) + ->and($persistent)->toContain(\App\Http\Middleware\EnsureCustomerActive::class) + ->and($persistent)->toContain(\App\Http\Middleware\RestrictAdminHost::class); +});