Harden Electron trust boundaries
Add centralized browser security helpers for restrictive BrowserWindow profiles, navigation denial, popup denial, permission denial, and exact HTTPS host allowlists. Apply those policies to the main renderer window and to the Real-Debrid and AllDebrid browser-login windows without weakening the credential renderer boundary. Add shared IPC sender validation for renderer handlers so development accepts only the local Vite origin and non-development accepts only the built file renderer tree. Route the registered renderer IPC handlers through the shared guard and restrict app:open-external to the same exact HTTPS allowlist. Cover the hardening with focused red-green tests for untrusted navigation, popup handling, permission requests, exact host/subdomain matching, packaged file renderer boundaries, untrusted IPC senders, and both browser-login window profiles.
This commit is contained in:
@@ -6,23 +6,27 @@ const {
|
||||
mockClearCache,
|
||||
mockFromPartition,
|
||||
mockBrowserWindow,
|
||||
mockBrowserWindowCtor,
|
||||
mockExecuteJavaScript,
|
||||
mockLoadURL,
|
||||
mockShow,
|
||||
mockFocus
|
||||
} = vi.hoisted(() => {
|
||||
const sessionFetch = vi.fn();
|
||||
const clearStorageData = vi.fn();
|
||||
const clearCache = vi.fn();
|
||||
const fromPartition = vi.fn();
|
||||
const executeJavaScript = vi.fn();
|
||||
const loadURL = vi.fn(async () => {});
|
||||
const show = vi.fn();
|
||||
const focus = vi.fn();
|
||||
const webContentsEvents: Record<string, (...args: unknown[]) => void> = {};
|
||||
const windowEvents: Record<string, (...args: unknown[]) => void> = {};
|
||||
let destroyed = false;
|
||||
mockBrowserWindowCtor,
|
||||
mockExecuteJavaScript,
|
||||
mockLoadURL,
|
||||
mockShow,
|
||||
mockFocus,
|
||||
mockSetWindowOpenHandler,
|
||||
mockSetPermissionRequestHandler
|
||||
} = vi.hoisted(() => {
|
||||
const sessionFetch = vi.fn();
|
||||
const clearStorageData = vi.fn();
|
||||
const clearCache = vi.fn();
|
||||
const fromPartition = vi.fn();
|
||||
const executeJavaScript = vi.fn();
|
||||
const loadURL = vi.fn(async () => {});
|
||||
const show = vi.fn();
|
||||
const focus = vi.fn();
|
||||
const setWindowOpenHandler = vi.fn();
|
||||
const setPermissionRequestHandler = vi.fn();
|
||||
const webContentsEvents: Record<string, (...args: unknown[]) => void> = {};
|
||||
const windowEvents: Record<string, (...args: unknown[]) => void> = {};
|
||||
let destroyed = false;
|
||||
|
||||
const browserWindow = {
|
||||
isDestroyed: vi.fn(() => destroyed),
|
||||
@@ -41,18 +45,22 @@ const {
|
||||
return browserWindow;
|
||||
}),
|
||||
webContents: {
|
||||
setUserAgent: vi.fn(),
|
||||
on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
|
||||
webContentsEvents[event] = handler;
|
||||
}),
|
||||
executeJavaScript
|
||||
}
|
||||
};
|
||||
setUserAgent: vi.fn(),
|
||||
setWindowOpenHandler,
|
||||
on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
|
||||
webContentsEvents[event] = handler;
|
||||
}),
|
||||
executeJavaScript,
|
||||
session: {
|
||||
setPermissionRequestHandler
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const BrowserWindowCtor = vi.fn(() => {
|
||||
destroyed = false;
|
||||
return browserWindow;
|
||||
});
|
||||
const BrowserWindowCtor = vi.fn((_options: unknown) => {
|
||||
destroyed = false;
|
||||
return browserWindow;
|
||||
});
|
||||
|
||||
return {
|
||||
mockSessionFetch: sessionFetch,
|
||||
@@ -61,19 +69,24 @@ const {
|
||||
mockFromPartition: fromPartition,
|
||||
mockBrowserWindow: browserWindow,
|
||||
mockBrowserWindowCtor: BrowserWindowCtor,
|
||||
mockExecuteJavaScript: executeJavaScript,
|
||||
mockLoadURL: loadURL,
|
||||
mockShow: show,
|
||||
mockFocus: focus
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("electron", () => ({
|
||||
session: {
|
||||
fromPartition: mockFromPartition
|
||||
},
|
||||
BrowserWindow: mockBrowserWindowCtor
|
||||
}));
|
||||
mockExecuteJavaScript: executeJavaScript,
|
||||
mockLoadURL: loadURL,
|
||||
mockShow: show,
|
||||
mockFocus: focus,
|
||||
mockSetWindowOpenHandler: setWindowOpenHandler,
|
||||
mockSetPermissionRequestHandler: setPermissionRequestHandler
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("electron", () => ({
|
||||
session: {
|
||||
fromPartition: mockFromPartition
|
||||
},
|
||||
BrowserWindow: mockBrowserWindowCtor,
|
||||
shell: {
|
||||
openExternal: vi.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
import { RealDebridWebFallback, extractPrivateTokenFromHtml } from "../src/main/realdebrid-web";
|
||||
|
||||
@@ -128,8 +141,20 @@ describe("realdebrid-web", () => {
|
||||
fileSize: 12345,
|
||||
retriesUsed: 0
|
||||
});
|
||||
expect(mockBrowserWindowCtor).toHaveBeenCalledTimes(1);
|
||||
expect(mockLoadURL).toHaveBeenCalledWith("https://real-debrid.com");
|
||||
expect(mockBrowserWindowCtor).toHaveBeenCalledTimes(1);
|
||||
expect(mockBrowserWindowCtor.mock.calls[0]?.[0]).toEqual(expect.objectContaining({
|
||||
webPreferences: {
|
||||
partition: "persist:realdebrid-web",
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: true,
|
||||
webSecurity: true,
|
||||
allowRunningInsecureContent: false
|
||||
}
|
||||
}));
|
||||
expect(mockSetWindowOpenHandler).toHaveBeenCalledTimes(1);
|
||||
expect(mockSetPermissionRequestHandler).toHaveBeenCalledTimes(1);
|
||||
expect(mockLoadURL).toHaveBeenCalledWith("https://real-debrid.com");
|
||||
expect(mockShow).toHaveBeenCalled();
|
||||
expect(mockFocus).toHaveBeenCalled();
|
||||
expect(mockSessionFetch).not.toHaveBeenCalled();
|
||||
|
||||
Reference in New Issue
Block a user