fix(startup): preserve Electron WebContents receivers

This commit is contained in:
Sucukdeluxe
2026-08-12 21:48:45 +02:00
parent d58c3a262f
commit ca015553f3
8 changed files with 96 additions and 26 deletions
+8
View File
@@ -2,6 +2,14 @@
All notable changes to Multi-Debrid Downloader are documented in this file. All notable changes to Multi-Debrid Downloader are documented in this file.
## [2.0.28] - 2026-08-12
### Startup reliability
- Fixed Electron security registration so native WebContents and session APIs retain their required receiver during application startup.
- Fixed packaged renderer IPC validation for case-insensitive Windows file paths.
- Added explicit startup and renderer-load error logging for future launch diagnostics.
## [2.0.27] - 2026-08-12 ## [2.0.27] - 2026-08-12
### Startup reliability ### Startup reliability
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "2.0.27", "version": "2.0.28",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "2.0.27", "version": "2.0.28",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"adm-zip": "0.6.0", "adm-zip": "0.6.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "2.0.27", "version": "2.0.28",
"description": "Desktop downloader", "description": "Desktop downloader",
"main": "build/main/main/main.js", "main": "build/main/main/main.js",
"author": "Sucukdeluxe", "author": "Sucukdeluxe",
+3 -3
View File
@@ -126,19 +126,19 @@ export function applyRemoteLoginSecurity(window: SecurityWindow, options: Remote
function applyCommonSecurity(window: SecurityWindow, externalHosts: readonly HttpsHostRule[]): void { function applyCommonSecurity(window: SecurityWindow, externalHosts: readonly HttpsHostRule[]): void {
const setWindowOpenHandler = window.webContents.setWindowOpenHandler as (handler: (details: { url: string }) => { action: "deny" }) => unknown; const setWindowOpenHandler = window.webContents.setWindowOpenHandler as (handler: (details: { url: string }) => { action: "deny" }) => unknown;
setWindowOpenHandler((details) => { setWindowOpenHandler.call(window.webContents, (details) => {
void openAllowedExternalUrl(details.url, externalHosts); void openAllowedExternalUrl(details.url, externalHosts);
return { action: "deny" }; return { action: "deny" };
}); });
const setPermissionRequestHandler = window.webContents.session.setPermissionRequestHandler as (handler: (webContents: unknown, permission: string, callback: (allowed: boolean) => void) => void) => unknown; const setPermissionRequestHandler = window.webContents.session.setPermissionRequestHandler as (handler: (webContents: unknown, permission: string, callback: (allowed: boolean) => void) => void) => unknown;
setPermissionRequestHandler((_webContents, _permission, callback) => { setPermissionRequestHandler.call(window.webContents.session, (_webContents, _permission, callback) => {
callback(false); callback(false);
}); });
} }
function onNavigation(window: SecurityWindow, event: "will-navigate" | "will-redirect", listener: (event: NavigationEvent, url: string) => void): void { 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; const on = window.webContents.on as (event: "will-navigate" | "will-redirect", listener: (event: NavigationEvent, url: string) => void) => unknown;
on(event, listener); on.call(window.webContents, event, listener);
} }
function isExpectedRendererUrl(rawUrl: string, expectedUrl: string): boolean { function isExpectedRendererUrl(rawUrl: string, expectedUrl: string): boolean {
+3 -1
View File
@@ -40,7 +40,9 @@ function isPackagedRendererUrl(senderUrl: string, appPath: string): boolean {
} }
const senderPath = path.resolve(fileURLToPath(parsed)); const senderPath = path.resolve(fileURLToPath(parsed));
const rendererPath = path.resolve(appPath, "build", "renderer"); const rendererPath = path.resolve(appPath, "build", "renderer");
return senderPath === rendererPath || senderPath.startsWith(rendererPath + path.sep); const actualPath = process.platform === "win32" ? senderPath.toLowerCase() : senderPath;
const trustedRendererPath = process.platform === "win32" ? rendererPath.toLowerCase() : rendererPath;
return actualPath === trustedRendererPath || actualPath.startsWith(trustedRendererPath + path.sep);
} catch { } catch {
return false; return false;
} }
+12 -2
View File
@@ -160,6 +160,10 @@ function createWindow(): BrowserWindow {
webPreferences: createMainWindowWebPreferences(path.join(__dirname, "../preload/preload.js")) webPreferences: createMainWindowWebPreferences(path.join(__dirname, "../preload/preload.js"))
}); });
window.webContents.on("did-fail-load", (_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
logger.error(`Renderer-Laden fehlgeschlagen: id=${window.id} code=${errorCode} mainFrame=${isMainFrame} url=${validatedURL} error=${errorDescription}`);
});
applyMainWindowSecurity(window, { applyMainWindowSecurity(window, {
rendererUrl: getTrustedRendererUrl(), rendererUrl: getTrustedRendererUrl(),
externalHosts: MAIN_WINDOW_EXTERNAL_HOSTS externalHosts: MAIN_WINDOW_EXTERNAL_HOSTS
@@ -182,9 +186,13 @@ function createWindow(): BrowserWindow {
window.setAutoHideMenuBar(true); window.setAutoHideMenuBar(true);
if (isDevMode()) { if (isDevMode()) {
void window.loadURL(DEV_SERVER_URL); void window.loadURL(DEV_SERVER_URL).catch((error) => {
logger.error(`Renderer-Start fehlgeschlagen: ${String(error?.stack || error)}`);
});
} else { } else {
void window.loadFile(path.join(app.getAppPath(), "build", "renderer", "index.html")); void window.loadFile(path.join(app.getAppPath(), "build", "renderer", "index.html")).catch((error) => {
logger.error(`Renderer-Start fehlgeschlagen: ${String(error?.stack || error)}`);
});
} }
return window; return window;
@@ -956,11 +964,13 @@ app.whenReady().then(() => {
} }
}); });
}).catch((error) => { }).catch((error) => {
logger.error(`App-Start fehlgeschlagen: ${String(error?.stack || error)}`);
console.error("App startup failed:", error); console.error("App startup failed:", error);
app.quit(); app.quit();
}); });
app.on("window-all-closed", () => { app.on("window-all-closed", () => {
logger.warn("Alle Hauptfenster geschlossen");
if (process.platform !== "darwin") { if (process.platform !== "darwin") {
app.quit(); app.quit();
} }
+31
View File
@@ -96,6 +96,37 @@ describe("browser-security", () => {
}); });
}); });
it("keeps native WebContents receivers when registering main-window security", () => {
const listeners = new Map<string, NavigationHandler>();
const webContents = {
on(this: unknown, event: string, listener: NavigationHandler) {
if (this !== webContents) {
throw new TypeError("WebContents receiver missing");
}
listeners.set(event, listener);
},
setWindowOpenHandler(this: unknown, _handler: WindowOpenHandler) {
if (this !== webContents) {
throw new TypeError("WebContents receiver missing");
}
},
session: {
setPermissionRequestHandler(this: unknown, _handler: PermissionHandler) {
if (this !== webContents.session) {
throw new TypeError("Session receiver missing");
}
}
}
};
expect(() => applyMainWindowSecurity({ webContents }, {
rendererUrl: "http://localhost:5180",
externalHosts: githubOnly
})).not.toThrow();
expect(listeners.has("will-navigate")).toBe(true);
expect(listeners.has("will-redirect")).toBe(true);
});
it("denies main-window navigation to an untrusted origin", () => { it("denies main-window navigation to an untrusted origin", () => {
const harness = createWindow(); const harness = createWindow();
applyMainWindowSecurity(harness.window, { applyMainWindowSecurity(harness.window, {
+19
View File
@@ -40,6 +40,25 @@ describe("ipc-security", () => {
})).not.toThrow(); })).not.toThrow();
}); });
it("accepts packaged renderer IPC despite Windows path casing differences", () => {
const platform = Object.getOwnPropertyDescriptor(process, "platform");
Object.defineProperty(process, "platform", { configurable: true, value: "win32" });
try {
const appPath = path.join("C:", "Program Files", "MDD", "resources", "app.asar");
const rendererUrl = "file:///c:/program%20files/mdd/resources/app.asar/build/renderer/index.html";
expect(() => assertTrustedIpcSender(eventFor(rendererUrl), {
isPackaged: true,
devServerUrl: "http://localhost:5180",
appPath
})).not.toThrow();
} finally {
if (platform) {
Object.defineProperty(process, "platform", platform);
}
}
});
it("rejects IPC from local files outside the packaged renderer tree", () => { it("rejects IPC from local files outside the packaged renderer tree", () => {
const appPath = path.join("C:", "Program Files", "MDD", "resources", "app.asar"); const appPath = path.join("C:", "Program Files", "MDD", "resources", "app.asar");
const attackerUrl = pathToFileURL(path.join("C:", "Users", "Public", "attacker.html")).toString(); const attackerUrl = pathToFileURL(path.join("C:", "Users", "Public", "attacker.html")).toString();