Skip to content

Strip the NOT_SERIALIZED marker in the session meta getters - #242

Merged
ralflang merged 2 commits into
horde:FRAMEWORK_6_0from
pierrefardel:fix/session-meta-decode
Oct 14, 2026
Merged

ralflang merged 2 commits into
horde:FRAMEWORK_6_0from
pierrefardel:fix/session-meta-decode

Conversation

@pierrefardel

@pierrefardel pierrefardel commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

HordeSession stores string values with a leading NOT_SERIALIZED byte
("\0"). getScoped() strips it, but the four session-meta getters read
$this->data directly and return the marker along with the value:

Getter Returned
getAuthenticatedUser() / getAuthId() "\0jdoe" instead of "jdoe"
getBrowserFingerprint(), getRemoteAddress() same leading \0

Impact

Every caller in Core uses the result as the user id for a preference lookup
(TopbarBuilder, SidebarBuilder, AccessKeyTrackerFactory,
ViewModeConfigurator, RenderingModeResolver, DesktopChromeRendererFactory).
With the marker, no stored preference is ever found on modern-stack pages
and the server defaults silently apply instead — the user's theme, the
access-key preference, and so on.

Found while testing #233: probing the theme resolution in a real request
showed it querying preferences for "\0pfard".

Change

Read these four values through getScoped('horde', …), like any other
scoped value. getAuthTimestamp() is left alone: integers are stored raw.

None of the callers compares these values to raw session data or uses them
for authentication decisions, so removing the marker cannot change who is
considered logged in.

Testing

On a FRAMEWORK_6_0 install, with the fix applied:

  • modern-stack pages now pick up the user's preferences: the access-key
    preference is read again (accesskeys.js gets loaded), and the user's
    theme is resolved instead of the server default (together with the
    companion theme-assets change);
  • classic pages kept rendering normally during the test.

Login and logout were not specifically exercised.

…ters

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The changes correctly reuse existing decoding logic, with only a non-blocking regression-test suggestion remaining.

1 open finding
What changed in this PR

Corrects session metadata decoding so modern-stack preference lookups receive user IDs without the storage marker.

Changes:

  • Routes four string metadata getters through getScoped().
  • Leaves integer timestamp handling unchanged.
File Description
src/​Session/​HordeSession.php Decodes stored user IDs, browser fingerprints, and remote addresses through the existing scoped reader.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

public function getAuthenticatedUser(): ?string
{
$value = $this->data['horde']['auth/userId'] ?? null;
$value = $this->getScoped('horde', 'auth/userId');

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, the existing tests seed unprefixed strings and passed with the bug. Added regression coverage in b6d6c18: a data provider over the four getters, values written through setScoped(), checked on the writing session and after a toPayload() round trip.

PHPUnit isn't available on my setup, so I replayed the same assertions in a standalone script against the installed dependencies: all 8 cases fail with the original class (each getter returns the value with a leading \0) and pass with the fix. CI will have the final word.

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().
@ralflang
ralflang merged commit b3333b0 into horde:FRAMEWORK_6_0 Oct 14, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants