Skip to content

fix(security): scope credentials to the instance origin, wipe on all session teardowns#37

Merged
SiteRelEnby merged 1 commit into
mainfrom
fix/credential-origin-and-session-teardown
Jul 15, 2026
Merged

fix(security): scope credentials to the instance origin, wipe on all session teardowns#37
SiteRelEnby merged 1 commit into
mainfrom
fix/credential-origin-and-session-teardown

Conversation

@SiteRelEnby

Copy link
Copy Markdown
Collaborator

Combined security PR from the latest review. Two credential-exposure problems, both confirmed against the code.

1. Credentials were origin-blind

AuthInterceptor attached Authorization: Bearer <token> and the Cloudflare Access secrets to every
request regardless of host. The Coil image client is built with okHttpClient.newBuilder().build(), so it
inherits that interceptor. And image URLs can resolve to three different places:

  • the API host (avatars served relative, resolved against the base URL)
  • a separate configured file_cdn_base host
  • a fully external URL: a remote avatar, or an image embedded in a member bio

So an external image host received the users live session token and CF-Access secrets. The trusted-device cookie had the same shape of bug: loadForRequestbound it tourl.hostfrom the request and gated only on the/v1/auth/` path, so it was host-blind.

Fix. Credentials are attached only to the instances own origins - the API base URL origin and the configured file CDN origin - via a shared isTrustedCredentialOrigincheck (scheme + host + port; path ignored). The trusted-device cookie is scoped to the API origin on both save and load. An external image host now receives only the non-secretX-Sheaf-Client` header.

The image client is still cloned from the API client (it needs the debug SSL-trust config for local dev
servers), but the origin gate means the inherited auth interceptor is a no-op for external hosts. Also dropped
the redundant second user-agent interceptor it was adding on top of the one it already inherits.

CDN note: I scoped credentials to API origin + configured CDN origin (not API-only), so instances that
serve avatars from their own CDN with auth keep working. External hosts get nothing either way.

2. Session teardown was incomplete

The wiper that clears the local cache and offline queue (from the earlier isolation PR) only ran on the normal
login and logout paths. Three teardowns bypassed it, all verified:

  • No-verification registration committed a session by calling prefs.saveTokens directly instead of
    finishAuth(), so a prior account`s cache and queue survived into the new account.
  • Changing the server URL while signed in cleared nothing - old tokens, cache, and queue all retained,
    and with the origin fix aside, the old bearer would otherwise ride to the new host.
  • Forced logout (a 401 on refresh) called clearTokens() only, leaving cache and queue behind.

All three now clear tokens and wipe. A server change to a different origin ends the session (a change to the
same origin is treated as a cosmetic edit and does nothing).

Tests

  • CredentialOriginTest: trusted API/CDN, external + lookalike hosts, scheme/port mismatch, path-prefix
    bases, case-insensitivity, scheme-less CDN, and the server-change (sameConfiguredOrigin) detection.
  • AuthInterceptorTest: the interceptors actual header output - bearer+CF on API and CDN, none on an external host, X-Sheaf-Client` always present, and the pending-token path.
  • TrustedDeviceCookieJarTest: extended for host scoping (not attached to another hosts /v1/auth/`, not
    stored from another host).

Verification

  • :app:assemblePlayRelease :wear:assembleRelease :app:testPlayReleaseUnitTest green; Hilt validated the new
    Lazy<AccountDataWiper> in TokenAuthenticator (no cycle). 162 phone tests pass.
  • Worth a device pass: sign in, load a member with an external-hosted avatar (confirm it still renders and,
    if you can watch traffic, carries no auth); change the server in Settings and confirm you are signed out;
    register on an instance with verification off and confirm no prior data lingers.

Follow-ups (tracked, not here)

Reliability cluster next: wear queue-drain mutex is per-instance (should be process-wide like the refresh
mutex), and SyncWorker uses ExistingWorkPolicy.REPLACE which can cancel a drain mid-createFront. Graph
O(n^2)-on-main (#172), editor drafts (#173), and home/journal out-of-order writes are separately tracked.

Origin-blind credentials. AuthInterceptor attached the bearer token and the
Cloudflare Access secrets to every request regardless of host, and the Coil
image client is cloned from the API client, so it inherited that interceptor.
Image URLs can point anywhere: a member avatar or an image embedded in a bio can
be hosted on an external server, and that server received the user's live
session. The trusted-device cookie was host-blind too (it bound to the request
host and gated only on the /v1/auth/ path).

Fix: credentials are attached only to the instance's own origins, the API base
URL and the configured file CDN, via a shared origin check. The cookie is scoped
to the API origin. External image hosts now get the X-Sheaf-Client header and
nothing else. The image client keeps the debug SSL-trust config (it is still
cloned from the API client) but no longer re-adds the user-agent interceptor it
already inherits.

ran only on the standard login and logout paths. Three teardowns bypassed it:
a registration that skips email verification (committed a session directly
without finishAuth), a server-URL change while signed in (cleared nothing, so
the old instance's tokens/cache/queue survived and its bearer would ride to the
new host), and a forced logout on refresh-token expiry (cleared tokens only).
the session; a change to the same origin does not.

Tests: origin matching (trusted/untrusted/lookalike/scheme/port/path-prefix),
the interceptor's actual header output for API vs CDN vs external hosts, and the
cookie's host+path scoping.
@SiteRelEnby
SiteRelEnby merged commit 208d42b into main Jul 15, 2026
1 check passed
@SiteRelEnby
SiteRelEnby deleted the fix/credential-origin-and-session-teardown branch July 15, 2026 02:46
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.

1 participant