From 10791bafb9c09b73e3066d1f8dca4a8b40b5bfea Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 6 Aug 2026 11:13:56 -0700 Subject: [PATCH] fix(browser): guard auth-host UA switch and cover it in tests did-start-navigation now reads the guest UA on every main-frame nav; the load-failure replay guest mocks had no WebContents UA surface, so the handler threw. Only touch the session UA when actually leaving an auth host (a Firefox override is in place), give the affected mocks a faithful UA surface, and add direct coverage for the auth-host switch, off-host restore, and native opt-out. --- src/main/browser/browser-manager.test.ts | 99 +++++++++++++++++++++++- src/main/browser/browser-manager.ts | 13 +++- 2 files changed, 105 insertions(+), 7 deletions(-) diff --git a/src/main/browser/browser-manager.test.ts b/src/main/browser/browser-manager.test.ts index fcda3846045..03dd54b6fc4 100644 --- a/src/main/browser/browser-manager.test.ts +++ b/src/main/browser/browser-manager.test.ts @@ -55,9 +55,17 @@ vi.mock('./popup-origin-bar-window', () => ({ })) import { browserManager } from './browser-manager' +import { googleAuthUserAgent } from './browser-google-auth-ua' describe('browserManager', () => { const rendererWebContentsId = 5001 + // Base (non-Firefox) UA a guest reports off the Google auth hosts. + const guestBaseUserAgent = 'Mozilla/5.0 (Test) Chrome/140.0.0.0' + const guestUaMethods = () => ({ + getUserAgent: vi.fn(() => guestBaseUserAgent), + setUserAgent: vi.fn(), + session: { getUserAgent: vi.fn(() => guestBaseUserAgent) } + }) type DownloadItemHandlerState = 'progressing' | 'interrupted' | 'completed' | 'cancelled' type DownloadItemHandler = (event: Electron.Event, state: DownloadItemHandlerState) => void @@ -1610,7 +1618,8 @@ describe('browserManager', () => { on: guestOnMock, off: guestOffMock, openDevTools: guestOpenDevToolsMock, - getURL: vi.fn(() => 'http://localhost:3000/') + getURL: vi.fn(() => 'http://localhost:3000/'), + ...guestUaMethods() } webContentsFromIdMock.mockImplementation((id: number) => { @@ -1686,7 +1695,8 @@ describe('browserManager', () => { on: guestOnMock, off: guestOffMock, openDevTools: guestOpenDevToolsMock, - getURL: vi.fn(() => 'https://example.com/') + getURL: vi.fn(() => 'https://example.com/'), + ...guestUaMethods() } webContentsFromIdMock.mockImplementation((id: number) => id === guest.id @@ -1738,7 +1748,8 @@ describe('browserManager', () => { off: guestOffMock, openDevTools: guestOpenDevToolsMock, send: vi.fn(), - getURL: vi.fn(() => 'chrome-error://chromewebdata/') + getURL: vi.fn(() => 'chrome-error://chromewebdata/'), + ...guestUaMethods() } webContentsFromIdMock.mockReturnValue(guest) @@ -1780,7 +1791,8 @@ describe('browserManager', () => { off: guestOffMock, openDevTools: guestOpenDevToolsMock, send: vi.fn(), - getURL: vi.fn(() => 'chrome-error://chromewebdata/') + getURL: vi.fn(() => 'chrome-error://chromewebdata/'), + ...guestUaMethods() } webContentsFromIdMock.mockReturnValue(guest) @@ -1819,6 +1831,85 @@ describe('browserManager', () => { expect(browserManager.getBrowserPageLoadError('browser-retry-abort-page')).toBeNull() }) + it('presents the Firefox UA on Google auth hosts and restores the base UA off them', () => { + let currentUa = guestBaseUserAgent + const setUserAgent = vi.fn((ua: string) => { + currentUa = ua + }) + const guest = { + id: 408, + isDestroyed: vi.fn(() => false), + getType: vi.fn(() => 'webview'), + setBackgroundThrottling: guestSetBackgroundThrottlingMock, + setWindowOpenHandler: guestSetWindowOpenHandlerMock, + on: guestOnMock, + off: guestOffMock, + openDevTools: guestOpenDevToolsMock, + getURL: vi.fn(() => 'https://accounts.google.com/'), + getUserAgent: vi.fn(() => currentUa), + setUserAgent, + session: { getUserAgent: vi.fn(() => guestBaseUserAgent) } + } + webContentsFromIdMock.mockReturnValue(guest) + + browserManager.attachGuestPolicies(guest as never) + browserManager.registerGuest({ + browserPageId: 'browser-auth-ua', + webContentsId: guest.id, + rendererWebContentsId + }) + const didStartNavigation = guestOnMock.mock.calls.find( + ([event]) => event === 'did-start-navigation' + )?.[1] as (event: unknown, url: string, isInPlace: boolean, isMainFrame: boolean) => void + setUserAgent.mockClear() + + didStartNavigation(null, 'https://accounts.google.com/v3/signin/identifier', false, true) + expect(setUserAgent).toHaveBeenLastCalledWith(googleAuthUserAgent()) + + // Off the auth host, the guest's base identity is restored. + didStartNavigation(null, 'https://myaccount.google.com/', false, true) + expect(setUserAgent).toHaveBeenLastCalledWith(guestBaseUserAgent) + + // A navigation that doesn't change the required UA must not thrash setUserAgent. + setUserAgent.mockClear() + didStartNavigation(null, 'https://example.com/', false, true) + expect(setUserAgent).not.toHaveBeenCalled() + }) + + it('leaves the UA untouched on Google auth hosts for native-UA profiles', () => { + const setUserAgent = vi.fn() + const guest = { + id: 409, + isDestroyed: vi.fn(() => false), + getType: vi.fn(() => 'webview'), + setBackgroundThrottling: guestSetBackgroundThrottlingMock, + setWindowOpenHandler: guestSetWindowOpenHandlerMock, + on: guestOnMock, + off: guestOffMock, + openDevTools: guestOpenDevToolsMock, + getURL: vi.fn(() => 'https://accounts.google.com/'), + getUserAgent: vi.fn(() => guestBaseUserAgent), + setUserAgent, + session: { getUserAgent: vi.fn(() => guestBaseUserAgent) } + } + webContentsFromIdMock.mockReturnValue(guest) + + browserManager.attachGuestPolicies(guest as never) + browserManager.registerGuest({ + browserPageId: 'browser-native-ua', + webContentsId: guest.id, + rendererWebContentsId, + userAgentMode: 'native' + }) + const didStartNavigation = guestOnMock.mock.calls.find( + ([event]) => event === 'did-start-navigation' + )?.[1] as (event: unknown, url: string, isInPlace: boolean, isMainFrame: boolean) => void + setUserAgent.mockClear() + + didStartNavigation(null, 'https://accounts.google.com/v3/signin/identifier', false, true) + expect(setUserAgent).not.toHaveBeenCalled() + }) + it('queues permission denials and download requests until the guest registers', () => { const rendererSendMock = vi.fn() const guest = { diff --git a/src/main/browser/browser-manager.ts b/src/main/browser/browser-manager.ts index 401f3f91f5c..c3f92eb9a46 100644 --- a/src/main/browser/browser-manager.ts +++ b/src/main/browser/browser-manager.ts @@ -926,9 +926,16 @@ export class BrowserManager { if (mode === 'native') { return } - const desiredUa = isGoogleAuthUrl(url) ? googleAuthUserAgent() : guest.session.getUserAgent() - if (guest.getUserAgent() !== desiredUa) { - guest.setUserAgent(desiredUa) + const firefoxUa = googleAuthUserAgent() + const currentUa = guest.getUserAgent() + if (isGoogleAuthUrl(url)) { + if (currentUa !== firefoxUa) { + guest.setUserAgent(firefoxUa) + } + } else if (currentUa === firefoxUa) { + // Only restore when the auth-host override is actually in place, so normal + // navigation never touches the session UA. + guest.setUserAgent(guest.session.getUserAgent()) } }