Fix Electron redirect and login boundary hardening
Apply the central Electron navigation policy to will-redirect in addition to will-navigate so main and remote-login windows block hostile redirect targets with the same exact host rules. Make main-window webPreferences explicitly keep webSecurity enabled and insecure content disabled, and make allowlisted external URL opening await shell.openExternal so IPC returns false on denied or failed opens without unhandled rejections. Expand focused coverage with hostile redirect cases, controlled shell failure handling, and AllDebrid Web behavior tests for existing-session generation plus login-required browser-window retry flow.
This commit is contained in:
@@ -11,10 +11,10 @@ type NavigationEvent = {
|
||||
|
||||
type SecurityWindow = {
|
||||
webContents: {
|
||||
on: (event: "will-navigate", listener: (event: NavigationEvent, url: string) => void) => unknown;
|
||||
setWindowOpenHandler: (handler: (details: { url: string }) => { action: "deny" }) => unknown;
|
||||
on: unknown;
|
||||
setWindowOpenHandler: unknown;
|
||||
session: {
|
||||
setPermissionRequestHandler: (handler: (webContents: unknown, permission: string, callback: (allowed: boolean) => void) => void) => unknown;
|
||||
setPermissionRequestHandler: unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -50,6 +50,8 @@ export function createMainWindowWebPreferences(preload: string): WebPreferences
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: true,
|
||||
webSecurity: true,
|
||||
allowRunningInsecureContent: false,
|
||||
preload
|
||||
};
|
||||
}
|
||||
@@ -82,46 +84,61 @@ export function isAllowedHttpsUrl(rawUrl: string, hosts: readonly HttpsHostRule[
|
||||
});
|
||||
}
|
||||
|
||||
export function openAllowedExternalUrl(rawUrl: string, hosts: readonly HttpsHostRule[]): boolean {
|
||||
export async function openAllowedExternalUrl(rawUrl: string, hosts: readonly HttpsHostRule[]): Promise<boolean> {
|
||||
if (!isAllowedHttpsUrl(rawUrl, hosts)) {
|
||||
return false;
|
||||
}
|
||||
void shell.openExternal(new URL(rawUrl).toString());
|
||||
return true;
|
||||
try {
|
||||
await shell.openExternal(new URL(rawUrl).toString());
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function applyMainWindowSecurity(window: SecurityWindow, options: MainWindowSecurityOptions): void {
|
||||
applyCommonSecurity(window, options.externalHosts);
|
||||
window.webContents.on("will-navigate", (event, url) => {
|
||||
const handleNavigation = (event: NavigationEvent, url: string): void => {
|
||||
if (isExpectedRendererUrl(url, options.rendererUrl)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
openAllowedExternalUrl(url, options.externalHosts);
|
||||
});
|
||||
void openAllowedExternalUrl(url, options.externalHosts);
|
||||
};
|
||||
onNavigation(window, "will-navigate", handleNavigation);
|
||||
onNavigation(window, "will-redirect", handleNavigation);
|
||||
}
|
||||
|
||||
export function applyRemoteLoginSecurity(window: SecurityWindow, options: RemoteLoginSecurityOptions): void {
|
||||
applyCommonSecurity(window, options.externalHosts);
|
||||
window.webContents.on("will-navigate", (event, url) => {
|
||||
const handleNavigation = (event: NavigationEvent, url: string): void => {
|
||||
if (isAllowedHttpsUrl(url, options.providerHosts)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
openAllowedExternalUrl(url, options.externalHosts);
|
||||
});
|
||||
void openAllowedExternalUrl(url, options.externalHosts);
|
||||
};
|
||||
onNavigation(window, "will-navigate", handleNavigation);
|
||||
onNavigation(window, "will-redirect", handleNavigation);
|
||||
}
|
||||
|
||||
function applyCommonSecurity(window: SecurityWindow, externalHosts: readonly HttpsHostRule[]): void {
|
||||
window.webContents.setWindowOpenHandler((details) => {
|
||||
openAllowedExternalUrl(details.url, externalHosts);
|
||||
const setWindowOpenHandler = window.webContents.setWindowOpenHandler as (handler: (details: { url: string }) => { action: "deny" }) => unknown;
|
||||
setWindowOpenHandler((details) => {
|
||||
void openAllowedExternalUrl(details.url, externalHosts);
|
||||
return { action: "deny" };
|
||||
});
|
||||
window.webContents.session.setPermissionRequestHandler((_webContents, _permission, callback) => {
|
||||
const setPermissionRequestHandler = window.webContents.session.setPermissionRequestHandler as (handler: (webContents: unknown, permission: string, callback: (allowed: boolean) => void) => void) => unknown;
|
||||
setPermissionRequestHandler((_webContents, _permission, callback) => {
|
||||
callback(false);
|
||||
});
|
||||
}
|
||||
|
||||
function onNavigation(window: SecurityWindow, event: "will-navigate" | "will-redirect", listener: (event: NavigationEvent, url: string) => void): void {
|
||||
const on = window.webContents.on as (event: "will-navigate" | "will-redirect", listener: (event: NavigationEvent, url: string) => void) => unknown;
|
||||
on(event, listener);
|
||||
}
|
||||
|
||||
function isExpectedRendererUrl(rawUrl: string, expectedUrl: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(String(rawUrl || ""));
|
||||
|
||||
Reference in New Issue
Block a user