From 644080a9c01057215838e43bc594cac42c3d46c9 Mon Sep 17 00:00:00 2001 From: lord_Tyrion Date: Wed, 20 May 2026 16:15:12 +0530 Subject: [PATCH] Enforce CORS for custom protocols --- .../cors-bypass-custom-protocols.ts | 36 ---- .../intercept-rules/custom-protocol-cors.ts | 161 ++++++++++++++++++ .../intercept-rules/index.ts | 6 +- 3 files changed, 164 insertions(+), 39 deletions(-) delete mode 100644 src/main/controllers/sessions-controller/intercept-rules/cors-bypass-custom-protocols.ts create mode 100644 src/main/controllers/sessions-controller/intercept-rules/custom-protocol-cors.ts diff --git a/src/main/controllers/sessions-controller/intercept-rules/cors-bypass-custom-protocols.ts b/src/main/controllers/sessions-controller/intercept-rules/cors-bypass-custom-protocols.ts deleted file mode 100644 index 743af5df9..000000000 --- a/src/main/controllers/sessions-controller/intercept-rules/cors-bypass-custom-protocols.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { createBetterWebRequest } from "@/controllers/sessions-controller/web-requests"; -import type { Session } from "electron"; - -/** - * Setup CORS bypass for whitelisted protocols - * @param session - */ -export function setupCorsBypassForCustomProtocols(session: Session) { - const webRequest = createBetterWebRequest(session.webRequest, "bypass-cors"); - - const WHITELISTED_PROTOCOLS = ["flow:", "flow-internal:"]; - - webRequest.onHeadersReceived((details, callback) => { - const currentUrl = details.webContents?.getURL(); - const protocol = URL.parse(currentUrl ?? "")?.protocol; - - if (protocol && WHITELISTED_PROTOCOLS.includes(protocol)) { - const newResponseHeaders = { ...details.responseHeaders }; - - // Remove all Access-Control-Allow-Origin headers in different cases - for (const header of Object.keys(newResponseHeaders)) { - if (header.toLowerCase() == "access-control-allow-origin") { - newResponseHeaders[header] = []; - } - } - - // Add the Access-Control-Allow-Origin header back with a wildcard - newResponseHeaders["Access-Control-Allow-Origin"] = ["*"]; - - callback({ responseHeaders: newResponseHeaders }); - return; - } - - callback({}); - }); -} diff --git a/src/main/controllers/sessions-controller/intercept-rules/custom-protocol-cors.ts b/src/main/controllers/sessions-controller/intercept-rules/custom-protocol-cors.ts new file mode 100644 index 000000000..be40f62f8 --- /dev/null +++ b/src/main/controllers/sessions-controller/intercept-rules/custom-protocol-cors.ts @@ -0,0 +1,161 @@ +import { createBetterWebRequest } from "@/controllers/sessions-controller/web-requests"; +import type { Session } from "electron"; + +const CUSTOM_PROTOCOLS = new Set(["flow:", "flow-internal:", "flow-external:"]); + +const ALLOWED_ORIGIN_PROTOCOLS_BY_TARGET_PROTOCOL: Record = { + "flow:": ["flow:", "flow-internal:"], + "flow-internal:": ["flow-internal:"], + "flow-external:": ["flow-external:"] +}; + +const CORS_RESPONSE_HEADERS = new Set([ + "access-control-allow-origin", + "access-control-allow-credentials", + "access-control-allow-headers", + "access-control-allow-methods", + "access-control-expose-headers" +]); + +type CorsRequestMetadata = { + origin: string; + requestedHeaders?: string; + requestedMethod?: string; +}; + +function getProtocol(url: string | undefined): string | null { + if (!url) return null; + + try { + return new URL(url).protocol; + } catch { + return null; + } +} + +function getHeader(headers: Record | undefined, name: string): string | undefined { + if (!headers) return undefined; + + const normalizedName = name.toLowerCase(); + for (const [headerName, headerValue] of Object.entries(headers)) { + if (headerName.toLowerCase() === normalizedName) { + return headerValue; + } + } + + return undefined; +} + +function stripCorsResponseHeaders(responseHeaders: Record | undefined) { + const headers = { ...(responseHeaders ?? {}) }; + + for (const header of Object.keys(headers)) { + if (CORS_RESPONSE_HEADERS.has(header.toLowerCase())) { + delete headers[header]; + } + } + + return headers; +} + +function appendVaryHeader(responseHeaders: Record, values: string[]) { + const existingHeader = Object.keys(responseHeaders).find((header) => header.toLowerCase() === "vary"); + const existingValues = existingHeader ? responseHeaders[existingHeader] : []; + const mergedValues = new Set(); + + for (const value of existingValues) { + value + .split(",") + .map((item) => item.trim()) + .filter(Boolean) + .forEach((item) => mergedValues.add(item)); + } + + values.forEach((value) => mergedValues.add(value)); + responseHeaders[existingHeader ?? "Vary"] = [Array.from(mergedValues).join(", ")]; +} + +function isAllowedCustomProtocolCorsRequest(targetUrl: string, origin: string | undefined) { + const targetProtocol = getProtocol(targetUrl); + if (!targetProtocol || !CUSTOM_PROTOCOLS.has(targetProtocol)) { + return true; + } + + if (!origin || origin === "null") { + return false; + } + + const originProtocol = getProtocol(origin); + if (!originProtocol) { + return false; + } + + return ALLOWED_ORIGIN_PROTOCOLS_BY_TARGET_PROTOCOL[targetProtocol]?.includes(originProtocol) ?? false; +} + +/** + * Apply a conservative CORS policy for Flow's custom protocols. + */ +export function setupCorsForCustomProtocols(session: Session) { + const webRequest = createBetterWebRequest(session.webRequest, "custom-protocol-cors"); + const corsRequests = new Map(); + + webRequest.onBeforeSendHeaders((details, callback) => { + const targetProtocol = getProtocol(details.url); + if (!targetProtocol || !CUSTOM_PROTOCOLS.has(targetProtocol)) { + callback({}); + return; + } + + const origin = getHeader(details.requestHeaders, "origin"); + if (!origin) { + callback({}); + return; + } + + if (!isAllowedCustomProtocolCorsRequest(details.url, origin)) { + callback({ cancel: true }); + return; + } + + corsRequests.set(details.id, { + origin, + requestedHeaders: getHeader(details.requestHeaders, "access-control-request-headers"), + requestedMethod: getHeader(details.requestHeaders, "access-control-request-method") + }); + + callback({}); + }); + + webRequest.onHeadersReceived((details, callback) => { + const targetProtocol = getProtocol(details.url); + if (!targetProtocol || !CUSTOM_PROTOCOLS.has(targetProtocol)) { + callback({}); + return; + } + + const corsRequest = corsRequests.get(details.id); + const responseHeaders = stripCorsResponseHeaders(details.responseHeaders); + + if (corsRequest && isAllowedCustomProtocolCorsRequest(details.url, corsRequest.origin)) { + responseHeaders["Access-Control-Allow-Origin"] = [corsRequest.origin]; + if (corsRequest.requestedMethod) { + responseHeaders["Access-Control-Allow-Methods"] = [corsRequest.requestedMethod]; + } + if (corsRequest.requestedHeaders) { + responseHeaders["Access-Control-Allow-Headers"] = [corsRequest.requestedHeaders]; + } + appendVaryHeader(responseHeaders, ["Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"]); + } + + callback({ responseHeaders }); + }); + + const cleanupRequest = (details: { id: number }) => { + corsRequests.delete(details.id); + }; + + webRequest.onCompleted(cleanupRequest); + webRequest.onErrorOccurred(cleanupRequest); + webRequest.onBeforeRedirect(cleanupRequest); +} diff --git a/src/main/controllers/sessions-controller/intercept-rules/index.ts b/src/main/controllers/sessions-controller/intercept-rules/index.ts index f07bbe9a9..8f64b310c 100644 --- a/src/main/controllers/sessions-controller/intercept-rules/index.ts +++ b/src/main/controllers/sessions-controller/intercept-rules/index.ts @@ -1,5 +1,5 @@ import { setupBetterPdfViewer } from "@/controllers/sessions-controller/intercept-rules/better-pdf-viewer"; -import { setupCorsBypassForCustomProtocols } from "@/controllers/sessions-controller/intercept-rules/cors-bypass-custom-protocols"; +import { setupCorsForCustomProtocols } from "@/controllers/sessions-controller/intercept-rules/custom-protocol-cors"; import { setupUserAgentTransformer } from "@/controllers/sessions-controller/intercept-rules/user-agent-transformer"; import type { Session } from "electron"; @@ -8,8 +8,8 @@ export function setupInterceptRules(session: Session) { // Transform the User-Agent header setupUserAgentTransformer(session); - // Bypass CORS for flow and flow-internal protocols - setupCorsBypassForCustomProtocols(session); + // Enforce CORS for Flow's custom protocols + setupCorsForCustomProtocols(session); // Setup redirects required for the better PDF viewer setupBetterPdfViewer(session);