
Cloud Vulnerability DB
A community-led vulnerabilities database
Locker::handle() returns $next($request) for every request that Livewire reports as a Livewire
request, before it reads the lockscreen session flag. Livewire decides that only from the
client-sent X-Livewire header. As a result, while a session is locked:
X-Livewire: 1.
Only plain page navigation is redirected to the lock screen.src/Http/Middleware/Locker.php (v3.4.1, also current 3.x e2db5ad), lines 22-24:
if ($this->isLivewireRequest()) {
return $next($request);
}isLivewireRequest() calls LivewireManager::isLivewireRequest(), which in Livewire 3 and 4 is
request()->hasHeader('X-Livewire').
The plugin registers Locker as persistent auth middleware
($panel->authMiddleware([Locker::class], true) in Lockscreen::register()). Livewire therefore
re-runs it on every POST /livewire/update, with a fake request carrying the method and path of
the page the component lives on (PersistentMiddleware::makeFakeRequest()); a redirect returned
there aborts the update (Utils::applyMiddleware()). That is exactly the mechanism the lock needs -
but the early return above skips the lock check on all of these requests, because the real
request carries the X-Livewire header.
The lock screen itself is not affected by a fix: its route (lockscreen.{panel}.page) only has the
panel's base middleware, not authMiddleware, so Locker never runs for the lock screen's own
Livewire updates.
Enforce the lock for Livewire requests as well; keep not counting them as idle activity:
if ($this->isLivewireRequest()) {
if ($this->isLocked($request) && $this->isSegmentMatched($request)) {
$panelId = filament()->getCurrentPanel()?->getId();
return to_route("lockscreen.{$panelId}.page");
}
return $next($request);
}plus a small isLocked() helper shared with the existing GET check (same truthiness as before).
The patch below also adds a first Pest + Orchestra Testbench test suite (tests/) with four
tests: a locked session redirects a normal visit, a visit with X-Livewire, and a Livewire action
of an already open page; actions still run while unlocked. Before the fix two of the four fail,
after the fix all four pass (see "Tests" below).
Run in a container on PHP 8.3.36 (Pest 3.8.7 / PHPUnit 11.5.56, Orchestra Testbench 10, Filament 5.11.1,
Livewire 4.4.8), with your existing phpunit.xml.dist:
Tests: 2 failed, 2 passed - "does not let the X-Livewire header skip the
lockscreen" and "blocks Livewire actions of an already open page while locked" get 200 instead of
the redirect;Tests: 4 passed (11 assertions);--test) on the changed files: clean except one existing issue in lines this patch does not
touch (blank_line_before_statement in the idle branch); Rector --dry-run on Locker.php: no changes.phpunit.xml.dist uses a deprecated schema (left unchanged).
Related, not part of this report: the idle timeout only fires on the next full page load, so a
page left open past the timeout stays usable until then; closing that would need a client-side
timer (a design choice for you).Laravel 13.40.0, Filament 5.11.1, Livewire 4.4.8, filament-lockscreen 3.4.1, PHP 8.3.36, in a
container without network; plugin set up as in the README (Lockscreen::make()->enablePlugin()),
one test page with a counter action. Requests go through the real HTTP kernel:
== 1. log in and open the panel page (unlocked baseline)
GET /admin/secret: 200, marker shown: yes
Livewire action while unlocked: 200, actionCount=1
== 2. lock the session with the package's own route (POST /admin/lock-session)
302 -> /admin/screen/lock, session lockscreen=true
== 3. CONTROL: normal browser GET of the panel page while locked
302 -> /admin/screen/lock => blocked (lock works)
== 4. BYPASS A: same GET with header 'X-Livewire: 1' while locked
200, marker shown: yes => page rendered despite lock
== 5. BYPASS B: Livewire action from the already-open page while locked
200, actionCount=2 => action ran despite lockManual check in a browser: lock the screen, press Back (or switch to another tab of the panel) and
use a table search or an action on that page - it works. Or, in the developer tools console:
fetch('/admin', {headers: {'X-Livewire': '1'}}) returns the dashboard HTML.
Someone with physical access to an unattended computer on which a panel user has locked the screen (manually or via the idle timeout) can keep using the panel with that user's permissions: read records, edit, delete, run actions. No tools are needed for the open-page route; the header route needs the browser's developer tools. No privilege escalation beyond the locked-out user, no remote attack without the victim's browser or session cookie. The lock screen is a convenience control and the OS lock remains the stronger one, hence Moderate rather than High. <details><summary>Plain-language explanation</summary>
Filament is a popular admin panel for Laravel websites. This plugin adds a lock screen to it:
when you step away from your desk you click "Lock", and the panel shows a password prompt instead
of your data. You stay logged in, so you do not lose your work - but anyone who walks up to your
computer has to know your password to get back in. It can also lock by itself after a period of
inactivity.
The lock is a note in your session on the server ("this session is locked"). A piece of code
called the Locker middleware looks at every request and, when the note is there, sends the
browser to the lock screen instead of the page it asked for.
Filament pages are built with Livewire. A page loads once; after that, every click inside it
(search a table, open a record, save a form, delete a row) is a small background request to the
server. Livewire marks those background requests with a label, the X-Livewire header.
The Locker middleware starts with: "Is this a Livewire request? Then let it through." - and
it does that before it checks whether the session is locked. Two consequences:
X-Livewire
header added: the full page comes back.
Only a normal page load (typing an address, clicking a regular link) is sent to the lock screen.An office locks its front door when the receptionist leaves, but staff badges still open the side door - and the side door does not check the badge, it only checks that the visitor is wearing a lanyard.
A small Laravel 13 + Filament 5 + Livewire 4 site was built in an isolated container with no network, the plugin installed exactly as its README says (version 3.4.1, the latest). A test user logged in, opened a test page, and locked the session with the plugin's own "Lock" button route. Then:
X-Livewire header -> the test page was shown,| Concern | Real? | Why |
|---|---|---|
| Someone at an unattended computer uses the panel although it is locked | Yes | Back button or another open tab is enough; no tools needed. Everything the logged-in user may do is possible: read records, edit, delete, run actions |
| Someone views any panel page while locked | Yes, with developer tools | Add the X-Livewire header to a page request |
| Attack from the internet without access to the computer | No | The attacker must already be in the victim's logged-in browser (or hold the session cookie, which is a bigger problem on its own) |
| Gaining more rights than the user has | No | The attacker acts as the locked-out user, with that user's permissions |
| Idle auto-lock protects an open page | Partly | Idle locking only happens on the next full page load; until then an open page stays usable (a design limit, mentioned separately) |
| Who is really hurt: the organisation whose admin panel it is - the plugin is installed precisely | ||
| so that a walked-away-from, logged-in admin screen cannot be used, and it does not deliver that. | ||
| The operating-system screen lock remains the stronger protection. |
The Locker middleware now checks the lock for Livewire requests too: when the session is locked,
a Livewire request is answered with the redirect to the lock screen instead of being run. The
lock screen itself still works, because its own page is not behind this middleware.
To verify: lock the screen, press Back, click something on the old page -> you land on the
lock screen. Unlock -> everything works again. Automated tests added with the fix check exactly
these cases.
</details>
<details><summary>Patch (git format-patch of the signed commit 9386d72, applies to 3.x e2db5ad / v3.4.1)</summary>
From 9386d72ec1dde1d1e2a1762f2a5e5bc4353c7b77 Mon Sep 17 00:00:00 2001
From: Iefken <ieffalot@hotmail.com>
Date: Tue, 10 Nov 2026 10:20:37 +0100
Subject: [PATCH] fix(middleware): enforce the lockscreen on Livewire requests
Locker passed every request that carries the X-Livewire header straight
to the next middleware, before looking at the lockscreen session flag.
Livewire component updates reach Locker through Livewire's persistent
middleware, so while the screen was locked every action on a page that
was still open (another tab, the back button) kept working, and any page
request could skip the lock by adding that header.
Livewire requests now get the same lock check as page visits: when the
session is locked they are redirected to the lockscreen (Livewire aborts
the update with that redirect). They still do not count as activity for
the idle timeout.
Tests: add a Pest + Testbench suite (tests/) with a fixture panel page.
It checks that a locked session redirects a normal visit, a visit with
the X-Livewire header and a Livewire action of an already open page, and
that actions still run while unlocked.
Signed-off-by: Iefken <ieffalot@hotmail.com>
---
src/Http/Middleware/Locker.php | 20 ++++++-
tests/Fixtures/SecretPage.php | 33 ++++++++++++
tests/Fixtures/TestPanelProvider.php | 41 ++++++++++++++
tests/Fixtures/User.php | 19 +++++++
tests/LockerTest.php | 80 ++++++++++++++++++++++++++++
tests/Pest.php | 5 ++
tests/TestCase.php | 58 ++++++++++++++++++++
7 files changed, 255 insertions(+), 1 deletion(-)
create mode 100644 tests/Fixtures/SecretPage.php
create mode 100644 tests/Fixtures/TestPanelProvider.php
create mode 100644 tests/Fixtures/User.php
create mode 100644 tests/LockerTest.php
create mode 100644 tests/Pest.php
create mode 100644 tests/TestCase.php
diff --git a/src/Http/Middleware/Locker.php b/src/Http/Middleware/Locker.php
index e74748b..0354073 100644
--- a/src/Http/Middleware/Locker.php
+++ b/src/Http/Middleware/Locker.php
@@ -19,14 +19,27 @@ class Locker
*/
public function handle(Request $request, Closure $next): mixed
{
+ /**
+ * Livewire requests (component updates) run this middleware through Livewire's persistent
+ * middleware, with the method and path of the page the component lives on. They must respect
+ * the lock too: otherwise every action on a page that is still open (another tab, the back
+ * button) keeps working while the screen is locked, and any request can skip the lock just
+ * by sending the "X-Livewire" header. They are not counted as user activity.
+ */
if ($this->isLivewireRequest()) {
+ if ($this->isLocked($request) && $this->isSegmentMatched($request)) {
+ $panelId = filament()->getCurrentPanel()?->getId();
+
+ return to_route("lockscreen.{$panelId}.page");
+ }
+
return $next($request);
}
/**
* Normal Lock session
*/
- if ($request->isMethod('GET') && ($request->session()->get('lockscreen') && (boolean) $request->session()->get('lockscreen') === true) && $this->isSegmentMatched($request)) {
+ if ($request->isMethod('GET') && $this->isLocked($request) && $this->isSegmentMatched($request)) {
$panelId = filament()->getCurrentPanel()?->getId();
redirect()->setIntendedUrl(url()->previous() ?? filament()->getDefaultPanel()->getPath());
@@ -57,6 +70,11 @@ class Locker
return $next($request);
}
+ protected function isLocked(Request $request): bool
+ {
+ return (bool) $request->session()->get('lockscreen') === true;
+ }
+
protected function isSegmentMatched($request): bool
{
$panelPath = filament()->getCurrentPanel()?->getPath();
diff --git a/tests/Fixtures/SecretPage.php b/tests/Fixtures/SecretPage.php
new file mode 100644
index 0000000..982d6f9
--- /dev/null
+++ b/tests/Fixtures/SecretPage.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace lockscreen\FilamentLockscreen\Tests\Fixtures;
+
+use Filament\Pages\Page;
+use Filament\Schemas\Components\Text;
+use Filament\Schemas\Schema;
+
+/**
+ * A panel page with one Livewire action, standing in for any page of a real panel.
+ */
+class SecretPage extends Page
+{
+ protected static ?string $slug = 'secret';
+
+ /** Lets the tests find this component's snapshot in the rendered page. */
+ public bool $isSecretPage = true;
+
+ /** How many times reveal() ran in this process. */
+ public static int $revealed = 0;
+
+ public function reveal(): void
+ {
+ static::$revealed++;
+ }
+
+ public function content(Schema $schema): Schema
+ {
+ return $schema->components([
+ Text::make('secret page content'),
+ ]);
+ }
+}
diff --git a/tests/Fixtures/TestPanelProvider.php b/tests/Fixtures/TestPanelProvider.php
new file mode 100644
index 0000000..12f7051
--- /dev/null
+++ b/tests/Fixtures/TestPanelProvider.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace lockscreen\FilamentLockscreen\Tests\Fixtures;
+
+use Filament\Http\Middleware\Authenticate;
+use Filament\Http\Middleware\AuthenticateSession;
+use Filament\Http\Middleware\DisableBladeIconComponents;
+use Filament\Http\Middleware\DispatchServingFilamentEvent;
+use Filament\Panel;
+use Filament\PanelProvider;
+use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
+use Illuminate\Cookie\Middleware\EncryptCookies;
+use Illuminate\Routing\Middleware\SubstituteBindings;
+use Illuminate\Session\Middleware\StartSession;
+use Illuminate\View\Middleware\ShareErrorsFromSession;
+use lockscreen\FilamentLockscreen\Lockscreen;
+
+class TestPanelProvider extends PanelProvider
+{
+ public function panel(Panel $panel): Panel
+ {
+ return $panel
+ ->default()
+ ->id('admin')
+ ->path('admin')
+ ->login()
+ ->pages([SecretPage::class])
+ ->plugins([Lockscreen::make()->enablePlugin()])
+ ->middleware([
+ EncryptCookies::class,
+ AddQueuedCookiesToResponse::class,
+ StartSession::class,
+ AuthenticateSession::class,
+ ShareErrorsFromSession::class,
+ SubstituteBindings::class,
+ DisableBladeIconComponents::class,
+ DispatchServingFilamentEvent::class,
+ ])
+ ->authMiddleware([Authenticate::class]);
+ }
+}
diff --git a/tests/Fixtures/User.php b/tests/Fixtures/User.php
new file mode 100644
index 0000000..c584e23
--- /dev/null
+++ b/tests/Fixtures/User.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace lockscreen\FilamentLockscreen\Tests\Fixtures;
+
+use Filament\Models\Contracts\FilamentUser;
+use Filament\Panel;
+use Illuminate\Foundation\Auth\User as Authenticatable;
+
+class User extends Authenticatable implements FilamentUser
+{
+ protected $table = 'users';
+
+ protected $guarded = [];
+
+ public function canAccessPanel(Panel $panel): bool
+ {
+ return true;
+ }
+}
diff --git a/tests/LockerTest.php b/tests/LockerTest.php
new file mode 100644
index 0000000..8ee1b26
--- /dev/null
+++ b/tests/LockerTest.php
@@ -0,0 +1,80 @@
+<?php
+
+use Livewire\Mechanisms\HandleRequests\HandleRequests;
+use lockscreen\FilamentLockscreen\Tests\Fixtures\SecretPage;
+use lockscreen\FilamentLockscreen\Tests\Fixtures\User;
+
+beforeEach(function (): void {
+ SecretPage::$revealed = 0;
+
+ $this->actingAs(User::query()->create([
+ 'name' => 'Test User',
+ 'email' => 'test@example.com',
+ 'password' => bcrypt('password'),
+ ]));
+});
+
+/**
+ * Render the secret page and return its Livewire snapshot, as an open browser tab holds it.
+ */
+function secretPageSnapshot(): string
+{
+ $html = test()->get('/admin/secret')->assertOk()->getContent();
+
+ preg_match_all('/wire:snapshot="([^"]+)"/', $html, $matches);
+
+ foreach ($matches[1] as $snapshot) {
+ $snapshot = html_entity_decode($snapshot, ENT_QUOTES);
+
+ if (array_key_exists('isSecretPage', json_decode($snapshot, true)['data'] ?? [])) {
+ return $snapshot;
+ }
+ }
+
+ throw new RuntimeException('No snapshot of the secret page found.');
+}
+
+/**
+ * Call reveal() on the secret page through the Livewire update endpoint, like the browser does.
+ */
+function callReveal(string $snapshot)
+{
+ return test()
+ ->withHeaders(['X-Livewire' => '1'])
+ ->postJson(app(HandleRequests::class)->getUpdateUri(), [
+ 'components' => [[
+ 'snapshot' => $snapshot,
+ 'updates' => new stdClass,
+ 'calls' => [['method' => 'reveal', 'params' => []]],
+ ]],
+ ]);
+}
+
+it('redirects a page visit to the lockscreen while locked', function (): void {
+ $this->withSession(['lockscreen' => true])
+ ->get('/admin/secret')
+ ->assertRedirect(route('lockscreen.admin.page'));
+});
+
+it('does not let the X-Livewire header skip the lockscreen', function (): void {
+ $this->withSession(['lockscreen' => true])
+ ->withHeaders(['X-Livewire' => '1'])
+ ->get('/admin/secret')
+ ->assertRedirect(route('lockscreen.admin.page'));
+});
+
+it('runs Livewire actions while unlocked', function (): void {
+ callReveal(secretPageSnapshot())->assertOk();
+
+ expect(SecretPage::$revealed)->toBe(1);
+});
+
+it('blocks Livewire actions of an already open page while locked', function (): void {
+ $snapshot = secretPageSnapshot();
+
+ $this->withSession(['lockscreen' => true]);
+
+ callReveal($snapshot)->assertRedirect(route('lockscreen.admin.page'));
+
+ expect(SecretPage::$revealed)->toBe(0);
+});
diff --git a/tests/Pest.php b/tests/Pest.php
new file mode 100644
index 0000000..60c830a
--- /dev/null
+++ b/tests/Pest.php
@@ -0,0 +1,5 @@
+<?php
+
+use lockscreen\FilamentLockscreen\Tests\TestCase;
+
+uses(TestCase::class)->in(__DIR__);
diff --git a/tests/TestCase.php b/tests/TestCase.php
new file mode 100644
index 0000000..293cc5e
--- /dev/null
+++ b/tests/TestCase.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace lockscreen\FilamentLockscreen\Tests;
+
+use BladeUI\Heroicons\BladeHeroiconsServiceProvider;
+use BladeUI\Icons\BladeIconsServiceProvider;
+use Filament\Actions\ActionsServiceProvider;
+use Filament\FilamentServiceProvider;
+use Filament\Forms\FormsServiceProvider;
+use Filament\Infolists\InfolistsServiceProvider;
+use Filament\Notifications\NotificationsServiceProvider;
+use Filament\Schemas\SchemasServiceProvider;
+use Filament\Support\SupportServiceProvider;
+use Filament\Tables\TablesServiceProvider;
+use Filament\Widgets\WidgetsServiceProvider;
+use Livewire\LivewireServiceProvider;
+use lockscreen\FilamentLockscreen\FilamentLockscreenServiceProvider;
+use lockscreen\FilamentLockscreen\Tests\Fixtures\TestPanelProvider;
+use lockscreen\FilamentLockscreen\Tests\Fixtures\User;
+use Orchestra\Testbench\TestCase as Orchestra;
+use RyanChandler\BladeCaptureDirective\BladeCaptureDirectiveServiceProvider;
+
+class TestCase extends Orchestra
+{
+ protected function getPackageProviders($app): array
+ {
+ return [
+ ActionsServiceProvider::class,
+ BladeCaptureDirectiveServiceProvider::class,
+ BladeHeroiconsServiceProvider::class,
+ BladeIconsServiceProvider::class,
+ FilamentServiceProvider::class,
+ FormsServiceProvider::class,
+ InfolistsServiceProvider::class,
+ LivewireServiceProvider::class,
+ NotificationsServiceProvider::class,
+ SchemasServiceProvider::class,
+ SupportServiceProvider::class,
+ TablesServiceProvider::class,
+ WidgetsServiceProvider::class,
+ FilamentLockscreenServiceProvider::class,
+ TestPanelProvider::class,
+ ];
+ }
+
+ protected function defineEnvironment($app): void
+ {
+ $app['config']->set('app.key', 'base64:'.base64_encode(str_repeat('k', 32)));
+ $app['config']->set('auth.providers.users.model', User::class);
+ $app['config']->set('database.default', 'testing');
+ $app['config']->set('session.driver', 'array');
+ }
+
+ protected function defineDatabaseMigrations(): void
+ {
+ $this->loadLaravelMigrations();
+ }
+}
--
2.45.1.windows.1</details> Issue found and fixed by Claude, then reviewed and confirmed by Ief (GitHub: iefken).
Source: NVD
Free Vulnerability Assessment
Evaluate your cloud security practices across 9 security domains to benchmark your risk level and identify gaps in your defenses.
Get a personalized demo
"Best User Experience I have ever seen, provides full visibility to cloud workloads."
"Wiz provides a single pane of glass to see what is going on in our cloud environments."
"We know that if Wiz identifies something as critical, it actually is."