From 3df77db6bc22f8c89f1980090b4ac2466108ba21 Mon Sep 17 00:00:00 2001 From: Fady Mondy Date: Tue, 15 Sep 2026 09:44:05 +0300 Subject: [PATCH 1/2] Escape the impersonation banner, skip streamed responses, and make canImpersonate safe without seeded permissions Co-Authored-By: Claude Opus 5 (1M context) --- src/Concerns/HasRolesAndPermissions.php | 6 ++- src/Http/Middleware/ImpersonationBanner.php | 16 ++++--- tests/Feature/ImpersonationHardeningTest.php | 45 ++++++++++++++++++++ 3 files changed, 59 insertions(+), 8 deletions(-) create mode 100644 tests/Feature/ImpersonationHardeningTest.php diff --git a/src/Concerns/HasRolesAndPermissions.php b/src/Concerns/HasRolesAndPermissions.php index 3023e92..123aa8c 100644 --- a/src/Concerns/HasRolesAndPermissions.php +++ b/src/Concerns/HasRolesAndPermissions.php @@ -44,8 +44,10 @@ public function canImpersonate(): bool return true; } - // Check if user has impersonate permission - if ($this->hasPermissionTo('impersonate users')) { + // Check if user has impersonate permission. checkPermissionTo() returns false instead of + // throwing PermissionDoesNotExist when the permission has not been seeded yet, which would + // otherwise crash every users table render (ImpersonateAction::visible calls this per row). + if ($this->checkPermissionTo('impersonate users')) { return true; } diff --git a/src/Http/Middleware/ImpersonationBanner.php b/src/Http/Middleware/ImpersonationBanner.php index 8776034..9881290 100644 --- a/src/Http/Middleware/ImpersonationBanner.php +++ b/src/Http/Middleware/ImpersonationBanner.php @@ -5,7 +5,9 @@ use Closure; use Illuminate\Http\Request; use Laravilt\Users\Services\ImpersonationService; +use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\StreamedResponse; class ImpersonationBanner { @@ -20,8 +22,8 @@ public function handle(Request $request, Closure $next): Response { $response = $next($request); - // Only inject banner for HTML responses - if (! $this->isHtmlResponse($response)) { + // Only inject banner for HTML responses (streamed/file responses cannot have their content replaced) + if ($response instanceof StreamedResponse || $response instanceof BinaryFileResponse || ! $this->isHtmlResponse($response)) { return $response; } @@ -61,8 +63,10 @@ protected function isHtmlResponse(Response $response): bool */ protected function renderBanner(): string { - $impersonator = $this->impersonationService->getImpersonator(); - $stopUrl = route('laravilt.users.stop-impersonation'); + // The name is user-controlled: escape it (and the rest) before injecting into the page + $impersonatorName = e($this->impersonationService->getImpersonator()?->name ?? ''); + $stopUrl = e(route('laravilt.users.stop-impersonation')); + $csrfToken = e($this->getCsrfToken()); return << - You are impersonating as {$impersonator?->name} + You are impersonating as {$impersonatorName}
- +