fix(electron): preserve trusted navigation location

This commit is contained in:
wiiiii123
2026-07-11 10:25:38 +07:00
parent b1759130a4
commit eb6a7faeb4
2 changed files with 65 additions and 5 deletions
+54
View File
@@ -206,6 +206,60 @@ describe("navigation event handlers", () => {
expect(on).toHaveBeenCalledWith("will-navigate", expect.any(Function));
expect(on).toHaveBeenCalledWith("will-redirect", expect.any(Function));
expect(on).toHaveBeenCalledWith("did-navigate", expect.any(Function));
expect(setWindowOpenHandler).toHaveBeenCalledWith(expect.any(Function));
});
it("does not trust a renderer-mutated URL as an exact reload", () => {
let currentUrl = "file:///opt/Recordly/dist/index.html?windowType=editor";
const on = vi.fn();
const webContents = {
getURL: () => currentUrl,
on,
setWindowOpenHandler: vi.fn(),
};
const openExternal = vi.fn(async () => undefined);
hardenWebContentsNavigation(webContents, openExternal);
// history.replaceState() changes getURL() without crossing a document-navigation boundary.
currentUrl = "file:///opt/Recordly/dist/index.html?windowType=source-selector";
const willNavigate = on.mock.calls.find(([eventName]) => eventName === "will-navigate")?.[1];
if (typeof willNavigate !== "function") {
throw new Error("will-navigate handler was not registered");
}
const preventDefault = vi.fn();
willNavigate({ url: currentUrl, preventDefault });
expect(preventDefault).toHaveBeenCalledOnce();
expect(openExternal).not.toHaveBeenCalled();
});
it("trusts an exact reload after a completed document navigation", () => {
const on = vi.fn();
const webContents = {
getURL: () => "",
on,
setWindowOpenHandler: vi.fn(),
};
hardenWebContentsNavigation(
webContents,
vi.fn(async () => undefined),
);
const didNavigate = on.mock.calls.find(([eventName]) => eventName === "did-navigate")?.[1];
const willNavigate = on.mock.calls.find(([eventName]) => eventName === "will-navigate")?.[1];
if (typeof didNavigate !== "function" || typeof willNavigate !== "function") {
throw new Error("navigation handlers were not registered");
}
const loadedUrl = "file:///opt/Recordly/dist/index.html?windowType=editor";
didNavigate({}, loadedUrl);
const preventDefault = vi.fn();
willNavigate({ url: loadedUrl, preventDefault });
expect(preventDefault).not.toHaveBeenCalled();
});
});
+11 -5
View File
@@ -88,21 +88,21 @@ const defaultReportOpenError: ReportOpenError = (url, error) => {
};
export function createWillNavigateHandler(
getCurrentUrl: () => string,
getTrustedRendererUrl: () => string,
openExternal: OpenExternal,
reportOpenError: ReportOpenError = defaultReportOpenError,
) {
return (event: NavigationEvent): void => {
const currentUrl = getCurrentUrl();
const trustedRendererUrl = getTrustedRendererUrl();
// Preserve an exact reload, but freeze all renderer-selected destination changes,
// including same-origin query mutations that can carry privileged local paths.
if (isExactRendererLocation(currentUrl, event.url)) {
if (isExactRendererLocation(trustedRendererUrl, event.url)) {
return;
}
// The internal-target check only prevents app URLs from leaking into the system browser.
event.preventDefault();
if (isInternalRendererTarget(currentUrl, event.url)) {
if (isInternalRendererTarget(trustedRendererUrl, event.url)) {
return;
}
@@ -134,9 +134,15 @@ export function hardenWebContentsNavigation(
openExternal: OpenExternal,
reportOpenError: ReportOpenError = defaultReportOpenError,
): void {
// Renderer history APIs mutate getURL() without a document navigation. Keep the last
// main-frame document URL as the reload trust boundary instead of trusting that live value.
let trustedRendererUrl = webContents.getURL();
webContents.on("did-navigate", (_event, url) => {
trustedRendererUrl = url;
});
webContents.on(
"will-navigate",
createWillNavigateHandler(() => webContents.getURL(), openExternal, reportOpenError),
createWillNavigateHandler(() => trustedRendererUrl, openExternal, reportOpenError),
);
webContents.on("will-redirect", createWillRedirectHandler());
webContents.setWindowOpenHandler(