mirror of
https://github.com/getpaseo/paseo.git
synced 2026-08-15 04:42:45 +00:00
Restrict desktop external URL schemes (#845)
This commit is contained in:
committed by
GitHub
parent
b9940e285c
commit
3f5acfff31
53
packages/desktop/src/features/opener.test.ts
Normal file
53
packages/desktop/src/features/opener.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { ipcMain, shell } from "electron";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { isAllowedExternalUrl, registerOpenerHandlers } from "./opener";
|
||||
|
||||
vi.mock("electron", () => ({
|
||||
ipcMain: { handle: vi.fn() },
|
||||
shell: { openExternal: vi.fn() },
|
||||
}));
|
||||
|
||||
function getRegisteredOpenUrlHandler(): (_event: unknown, url: unknown) => Promise<void> {
|
||||
registerOpenerHandlers();
|
||||
const handler = vi.mocked(ipcMain.handle).mock.calls.find(([channel]) => {
|
||||
return channel === "paseo:opener:openUrl";
|
||||
})?.[1];
|
||||
if (typeof handler !== "function") {
|
||||
throw new Error("open URL handler was not registered");
|
||||
}
|
||||
return handler as (_event: unknown, url: unknown) => Promise<void>;
|
||||
}
|
||||
|
||||
describe("desktop opener", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(ipcMain.handle).mockReset();
|
||||
vi.mocked(shell.openExternal).mockReset();
|
||||
});
|
||||
|
||||
it("allows only http and https external URLs", () => {
|
||||
expect(isAllowedExternalUrl("https://example.com/path")).toBe(true);
|
||||
expect(isAllowedExternalUrl("http://localhost:8081")).toBe(true);
|
||||
expect(isAllowedExternalUrl("file:///etc/passwd")).toBe(false);
|
||||
expect(isAllowedExternalUrl("javascript:alert(1)")).toBe(false);
|
||||
expect(isAllowedExternalUrl("paseo://settings")).toBe(false);
|
||||
expect(isAllowedExternalUrl("/relative/path")).toBe(false);
|
||||
expect(isAllowedExternalUrl(null)).toBe(false);
|
||||
});
|
||||
|
||||
it("opens allowed URLs through Electron shell", async () => {
|
||||
const handler = getRegisteredOpenUrlHandler();
|
||||
|
||||
await handler({}, "https://example.com");
|
||||
|
||||
expect(shell.openExternal).toHaveBeenCalledWith("https://example.com");
|
||||
});
|
||||
|
||||
it("rejects blocked URLs before invoking Electron shell", async () => {
|
||||
const handler = getRegisteredOpenUrlHandler();
|
||||
|
||||
await expect(handler({}, "file:///etc/passwd")).rejects.toThrow("Unsupported external URL");
|
||||
|
||||
expect(shell.openExternal).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,25 @@
|
||||
import { shell, ipcMain } from "electron";
|
||||
|
||||
const ALLOWED_EXTERNAL_URL_PROTOCOLS = new Set(["http:", "https:"]);
|
||||
|
||||
export function isAllowedExternalUrl(value: unknown): value is string {
|
||||
if (typeof value !== "string") {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return ALLOWED_EXTERNAL_URL_PROTOCOLS.has(url.protocol);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function registerOpenerHandlers(): void {
|
||||
ipcMain.handle("paseo:opener:openUrl", async (_event, url: string) => {
|
||||
ipcMain.handle("paseo:opener:openUrl", async (_event, url: unknown) => {
|
||||
if (!isAllowedExternalUrl(url)) {
|
||||
throw new Error("Unsupported external URL");
|
||||
}
|
||||
await shell.openExternal(url);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user