From 97af58dce360a83af6eb0afb1dcbf757cf8e1768 Mon Sep 17 00:00:00 2001 From: Fardel Pierre Date: Thu, 8 Oct 2026 14:55:28 +0200 Subject: [PATCH 1/2] fix(session): strip the NOT_SERIALIZED marker in the session meta getters HordeSession stores strings with a leading NOT_SERIALIZED byte ("\0"). getScoped() removes it, but getAuthenticatedUser(), getAuthId(), getBrowserFingerprint() and getRemoteAddress() read $this->data raw and returned the marker with the value, e.g. "\0jdoe" instead of "jdoe". Every caller uses the result as a user id for preference lookups (topbar, sidebar, access keys, view mode, theme), so on modern-stack pages no user preference was ever found and the server defaults applied instead. Read them through getScoped() like any other scoped value. --- src/Session/HordeSession.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Session/HordeSession.php b/src/Session/HordeSession.php index adb40529..d9d660b9 100644 --- a/src/Session/HordeSession.php +++ b/src/Session/HordeSession.php @@ -306,28 +306,28 @@ public function clearScopeWithPrefixes(string $app, array $prefixes): void public function getAuthenticatedUser(): ?string { - $value = $this->data['horde']['auth/userId'] ?? null; + $value = $this->getScoped('horde', 'auth/userId'); return is_string($value) ? $value : null; } public function getAuthId(): ?string { - $value = $this->data['horde']['auth/authId'] ?? null; + $value = $this->getScoped('horde', 'auth/authId'); return is_string($value) ? $value : null; } public function getBrowserFingerprint(): ?string { - $value = $this->data['horde']['auth/browser'] ?? null; + $value = $this->getScoped('horde', 'auth/browser'); return is_string($value) ? $value : null; } public function getRemoteAddress(): ?string { - $value = $this->data['horde']['auth/remoteAddr'] ?? null; + $value = $this->getScoped('horde', 'auth/remoteAddr'); return is_string($value) ? $value : null; } From b6d6c189d3b4d4fe59b7db405aab7dacdd1d258a Mon Sep 17 00:00:00 2001 From: Fardel Pierre Date: Thu, 8 Oct 2026 15:03:48 +0200 Subject: [PATCH 2/2] test(session): cover the meta getters with marker-prefixed strings The existing getter tests seed the session with unprefixed strings, so they passed with the NOT_SERIALIZED marker leaking out. Write the values through setScoped() instead, and check each getter both on the writing session and after a round trip through toPayload(). --- test/Unit/Session/HordeSessionTest.php | 42 ++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/test/Unit/Session/HordeSessionTest.php b/test/Unit/Session/HordeSessionTest.php index 0561bfd3..acf92cac 100644 --- a/test/Unit/Session/HordeSessionTest.php +++ b/test/Unit/Session/HordeSessionTest.php @@ -18,6 +18,7 @@ use Horde\SessionHandler\Session; use Horde\SessionHandler\SessionId; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use Horde_Pack; @@ -221,6 +222,47 @@ public function testGetRemoteAddress(): void self::assertSame('192.168.1.1', $session->getRemoteAddress()); } + /** + * Strings written through setScoped() carry the NOT_SERIALIZED marker + * on the wire. The meta getters must return them without it, both on + * the writing session and after a round trip through toPayload(). + * The tests above seed $data with unprefixed strings, so they did not + * catch the marker leaking out. + * + * @return array + */ + public static function metaGetterProvider(): array + { + return [ + 'authenticated user' => ['auth/userId', 'getAuthenticatedUser', 'alice'], + 'auth id' => ['auth/authId', 'getAuthId', 'alice@example.com'], + 'browser fingerprint' => ['auth/browser', 'getBrowserFingerprint', 'Mozilla/5.0'], + 'remote address' => ['auth/remoteAddr', 'getRemoteAddress', '192.168.1.1'], + ]; + } + + #[Test] + #[DataProvider('metaGetterProvider')] + public function testMetaGetterStripsStorageMarker(string $key, string $getter, string $value): void + { + $session = $this->createSession(); + $session->setScoped('horde', $key, $value); + + self::assertSame($value, $session->{$getter}()); + } + + #[Test] + #[DataProvider('metaGetterProvider')] + public function testMetaGetterStripsStorageMarkerAfterPayloadRoundTrip(string $key, string $getter, string $value): void + { + $writer = $this->createSession(); + $writer->setScoped('horde', $key, $value); + + $reader = $this->createSession($writer->toPayload()); + + self::assertSame($value, $reader->{$getter}()); + } + #[Test] public function testGetAuthTimestamp(): void {