Skip to content

fix(security): plug credential leaks via redirects, foreign 401s, and the server-switch race#40

Merged
SiteRelEnby merged 1 commit into
mainfrom
fix/credential-redirect-authenticator-guard
Jul 15, 2026
Merged

fix(security): plug credential leaks via redirects, foreign 401s, and the server-switch race#40
SiteRelEnby merged 1 commit into
mainfrom
fix/credential-redirect-authenticator-guard

Conversation

@SiteRelEnby

Copy link
Copy Markdown
Collaborator

Independent blast-radius assessment turned up three credential-exposure paths that the AuthInterceptor
origin-scoping (#37/#39) does not cover, because they live below the application-interceptor layer or in a
different component. All three verified against the current code.

1. TokenAuthenticator was origin-blind (the live one)

The shared authenticated client is handed to widgets via WidgetEntryPoint.okHttpClient(), and
WidgetAvatars.fetchSquareBitmap uses it to download avatar bytes from arbitrary hosts. AuthInterceptor
(from #37) wont *proactively* attach the bearer to an external host - but TokenAuthenticatoris a separate OkHttpAuthenticator`, and it was origin-blind. An external avatar host returning 401 made it refresh the
token and retry that request to the external host with the freshly minted bearer.

Fix: it now returns null for any 401 whose request isn`t the API origin - no refresh, no retry, no leak, and
no wasted refresh-token rotation.

2. Cross-origin redirects kept the CF-Access headers

AuthInterceptor is an application interceptor, so it only sees the original request. On an
API -> external redirect, OkHttp 4.12 strips Authorization but not custom headers, so CF-Access-* followed
to the external host.

Fix: CredentialGuardInterceptor, a network interceptor, which runs on every hop - redirect follow-ups
and authenticator retries included - and strips Authorization, CF-Access-Client-Id/Secret, and Cookie
from any request not going to the API origin. This is also a belt for (1).

3. Server-switch race (save-before-clear)

Both saveBaseUrl paths saved the new base URL before clearing the old session, leaving a window where the
new origin was configured but the old token was still present - so a concurrent request would be treated as
trusted and carry the old bearer to the new host. Now both (AuthViewModel, SettingsViewModel) clear tokens

  • wipe before storing the new origin.

Why a network interceptor rather than re-pointing each caller

Widgets are only one consumer of the shared client. A per-hop network guard means any current or future use
that touches a foreign host - redirect, retry, or a new caller - cannot leak the session, without having to
find and re-point every call site. AuthInterceptor stays as the proactive scoping; this is the backstop.

Tests

  • CredentialGuardInterceptorTest: strips all creds on a foreign/CDN hop, preserves them on the API origin,
    leaves non-credential headers alone.
  • TokenAuthenticatorTest: a foreign-host (and CDN-host) 401 returns null without touching the refresh token.
  • :app:assemblePlayRelease :app:testPlayReleaseUnitTest green, 166 tests.

No new user-facing behaviour - this hardens the "credentials only go to your server" promise already in the
changelog against three edge cases, so no changelog change.

…and the server-switch race

Three residual credential-exposure paths that the AuthInterceptor origin-scoping
didn't cover, because they happen below the application interceptor layer or in a
different component.

1. TokenAuthenticator was origin-blind. The shared authenticated client is used
   to fetch avatars (WidgetAvatars, via WidgetEntryPoint.okHttpClient), and those
   URLs can point at an external host. AuthInterceptor won't proactively attach a
   bearer there, but a 401 from that host made the authenticator refresh the token
   and retry the request to the external host with the freshly minted bearer. It
   now returns null for any 401 whose request isn't the API origin: no refresh, no
   retry, no leak.

2. Cross-origin redirects kept the CF-Access headers. AuthInterceptor is an
   application interceptor, so it only sees the original request; on an
   API -> external redirect, OkHttp strips Authorization but not custom headers,
   so CF-Access-* followed to the external host. Added CredentialGuardInterceptor
   as a NETWORK interceptor, which runs on every hop (redirect follow-ups and
   authenticator retries included) and strips Authorization, CF-Access-*, and
   Cookie from any request not going to the API origin. This is also a belt for
   (1).

3. Changing the server saved the new base URL before clearing the old session,
   leaving a window where the new origin was configured but the old token was
   still present, so a concurrent request would be treated as trusted and carry
   the old bearer to the new host. Both saveBaseUrl paths (AuthViewModel,

Tests: the guard strips creds on foreign/CDN hops and preserves them on the API
origin; the authenticator ignores foreign-host 401s without touching the refresh
token.
@SiteRelEnby
SiteRelEnby enabled auto-merge July 15, 2026 03:39
@SiteRelEnby
SiteRelEnby merged commit 6a9a4c0 into main Jul 15, 2026
1 check passed
@SiteRelEnby
SiteRelEnby deleted the fix/credential-redirect-authenticator-guard branch July 15, 2026 03:43
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