fix(security): plug credential leaks via redirects, foreign 401s, and the server-switch race#40
Merged
Conversation
…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
enabled auto-merge
July 15, 2026 03:39
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Independent blast-radius assessment turned up three credential-exposure paths that the
AuthInterceptororigin-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.
TokenAuthenticatorwas origin-blind (the live one)The shared authenticated client is handed to widgets via
WidgetEntryPoint.okHttpClient(), andWidgetAvatars.fetchSquareBitmapuses it to download avatar bytes from arbitrary hosts.AuthInterceptor(from #37) won
t *proactively* attach the bearer to an external host - butTokenAuthenticatoris a separate OkHttpAuthenticator`, and it was origin-blind. An external avatar host returning 401 made it refresh thetoken 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
AuthInterceptoris an application interceptor, so it only sees the original request. On anAPI -> external redirect, OkHttp 4.12 strips
Authorizationbut not custom headers, soCF-Access-*followedto the external host.
Fix:
CredentialGuardInterceptor, a network interceptor, which runs on every hop - redirect follow-upsand authenticator retries included - and strips
Authorization,CF-Access-Client-Id/Secret, andCookiefrom any request not going to the API origin. This is also a belt for (1).
3. Server-switch race (save-before-clear)
Both
saveBaseUrlpaths saved the new base URL before clearing the old session, leaving a window where thenew 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 tokensWhy 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.
AuthInterceptorstays 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:testPlayReleaseUnitTestgreen, 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.