From 9103c4ade5cc65bbb5b48d5188b4c50776cecd6a Mon Sep 17 00:00:00 2001 From: Gilberto <37253958+gilsmt@users.noreply.github.com> Date: Mon, 21 Sep 2026 04:37:30 +0000 Subject: [PATCH] fix(extension-ingest): restrict credentialed CORS to the app's own hosts Co-authored-by: polylane[bot] <277585245+polylane[bot]@users.noreply.github.com> --- .../extension-ingest/origins.test.ts | 50 +++++++++++++++++++ lib/integrations/extension-ingest/origins.ts | 27 ++++++++++ lib/integrations/extension-ingest/route.ts | 23 ++------- 3 files changed, 82 insertions(+), 18 deletions(-) create mode 100644 lib/integrations/extension-ingest/origins.test.ts create mode 100644 lib/integrations/extension-ingest/origins.ts diff --git a/lib/integrations/extension-ingest/origins.test.ts b/lib/integrations/extension-ingest/origins.test.ts new file mode 100644 index 00000000..0ae7153f --- /dev/null +++ b/lib/integrations/extension-ingest/origins.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, test } from "bun:test"; +import { + isChromeExtensionOrigin, + isTrustedCacheWebOrigin, +} from "@/lib/integrations/extension-ingest/origins"; + +describe("isTrustedCacheWebOrigin", () => { + test("accepts the app's own hosts", () => { + expect(isTrustedCacheWebOrigin("https://cachd.app")).toBe(true); + expect(isTrustedCacheWebOrigin("https://www.cachd.app")).toBe(true); + }); + + test("accepts localhost on any port", () => { + expect(isTrustedCacheWebOrigin("http://localhost:3000")).toBe(true); + expect(isTrustedCacheWebOrigin("http://localhost:8080")).toBe(true); + }); + + test("rejects cachd.app subdomains served by third parties", () => { + expect(isTrustedCacheWebOrigin("https://docs.cachd.app")).toBe(false); + expect(isTrustedCacheWebOrigin("https://preview.cachd.app")).toBe( + false + ); + }); + + test("rejects unassigned cachd.app labels and lookalikes", () => { + expect(isTrustedCacheWebOrigin("https://api.cachd.app")).toBe(false); + expect(isTrustedCacheWebOrigin("https://cachd.app.evil.com")).toBe( + false + ); + expect(isTrustedCacheWebOrigin("https://evilcachd.app")).toBe(false); + expect(isTrustedCacheWebOrigin("http://cachd.app")).toBe(false); + }); +}); + +describe("isChromeExtensionOrigin", () => { + test("accepts a chrome extension origin", () => { + expect( + isChromeExtensionOrigin( + "chrome-extension://abcdefghijklmnopabcdefghijklmnop" + ) + ).toBe(true); + }); + + test("rejects other origins", () => { + expect(isChromeExtensionOrigin("https://cachd.app")).toBe(false); + expect(isChromeExtensionOrigin("chrome-extension://not-an-id")).toBe( + false + ); + }); +}); diff --git a/lib/integrations/extension-ingest/origins.ts b/lib/integrations/extension-ingest/origins.ts new file mode 100644 index 00000000..fbcfe3d5 --- /dev/null +++ b/lib/integrations/extension-ingest/origins.ts @@ -0,0 +1,27 @@ +/** + * The app's own web origins. The extension reads the ingest token from the + * page origin, so only these hosts are first-party for CORS. A wildcard once + * trusted every cachd.app label, including subdomains delegated to third-party + * services, which let those hosts read a signed-in user's ingest token. + */ +const TRUSTED_CACHE_WEB_ORIGINS = new Set([ + "https://cachd.app", + "https://www.cachd.app", +]); + +/** Local development serves the app on arbitrary localhost ports. */ +const LOCALHOST_ORIGIN_PATTERN = /^http:\/\/localhost:\d+$/; + +/** Chrome extension service-worker origins. Extension ids are `[a-p]{32}`. */ +const CHROME_EXTENSION_ORIGIN_PATTERN = /^chrome-extension:\/\/[a-p]{32}$/; + +export function isTrustedCacheWebOrigin(origin: string): boolean { + return ( + TRUSTED_CACHE_WEB_ORIGINS.has(origin) || + LOCALHOST_ORIGIN_PATTERN.test(origin) + ); +} + +export function isChromeExtensionOrigin(origin: string): boolean { + return CHROME_EXTENSION_ORIGIN_PATTERN.test(origin); +} diff --git a/lib/integrations/extension-ingest/route.ts b/lib/integrations/extension-ingest/route.ts index 7d0494a0..57ae66ac 100644 --- a/lib/integrations/extension-ingest/route.ts +++ b/lib/integrations/extension-ingest/route.ts @@ -2,6 +2,10 @@ import "server-only"; import * as z from "zod"; import { createLogger } from "@/lib/common/logs/console/logger"; +import { + isChromeExtensionOrigin, + isTrustedCacheWebOrigin, +} from "@/lib/integrations/extension-ingest/origins"; import { resolveExtensionIngestUserId } from "@/lib/integrations/extension-ingest/service"; const log = createLogger("integrations:extension-ingest"); @@ -22,20 +26,6 @@ const TOKEN_CORS_HEADERS = { "Access-Control-Max-Age": "86400", } as const; -const TRUSTED_CACHE_WEB_ORIGIN_PATTERNS = [ - /^https:\/\/cachd\.app$/, - /^https:\/\/[a-z0-9-]+\.cachd\.app$/, - /^http:\/\/localhost:\d+$/, -]; - -const CHROME_EXTENSION_ORIGIN_PATTERN = /^chrome-extension:\/\/[a-p]{32}$/; - -function isTrustedCacheWebOrigin(origin: string): boolean { - return TRUSTED_CACHE_WEB_ORIGIN_PATTERNS.some((pattern) => - pattern.test(origin) - ); -} - function trustedOriginForRequest( request: Request, config: { allowChromeExtensionOrigin: boolean } @@ -47,10 +37,7 @@ function trustedOriginForRequest( if (isTrustedCacheWebOrigin(origin)) { return origin; } - if ( - config.allowChromeExtensionOrigin && - CHROME_EXTENSION_ORIGIN_PATTERN.test(origin) - ) { + if (config.allowChromeExtensionOrigin && isChromeExtensionOrigin(origin)) { return origin; } return null;