From 4165774667ddc22a849fdaff7b55ba6dd2594c11 Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Thu, 21 May 2026 23:01:20 +0200 Subject: [PATCH] fix(websub): validate callback URL (HTTPS), fix domain regex, enforce secret length --- src/routes/hub.ts | 23 ++++++++++++++++++++++- tsconfig.json | 4 ++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/routes/hub.ts b/src/routes/hub.ts index b4bab38..c5af93f 100644 --- a/src/routes/hub.ts +++ b/src/routes/hub.ts @@ -33,6 +33,14 @@ hubRouter.post("/", async (c) => { ); } + if ( + typeof mode !== "string" || + typeof topic !== "string" || + typeof callbackUrl !== "string" + ) { + return c.text("Bad Request: unexpected field types", 400); + } + if (mode !== "subscribe" && mode !== "unsubscribe") { return c.text( "Bad Request: hub.mode must be subscribe or unsubscribe", @@ -40,9 +48,19 @@ hubRouter.post("/", async (c) => { ); } + let parsedCallback: URL; + try { + parsedCallback = new URL(callbackUrl); + } catch { + return c.text("Bad Request: hub.callback must be a valid URL", 400); + } + if (parsedCallback.protocol !== "https:") { + return c.text("Bad Request: hub.callback must use HTTPS", 400); + } + // Validate that topic matches a known RSS feed on this hub const topicPattern = new RegExp( - `^https://${env.DOMAIN.replace(".", "\\.")}/rss/([^/]+)$`, + `^https://${env.DOMAIN.replaceAll(".", "\\.")}/rss/([^/]+)$`, ); const match = topic.match(topicPattern); if (!match) { @@ -54,6 +72,9 @@ hubRouter.post("/", async (c) => { const feedId = match[1]; const secret = form.get("hub.secret") ?? undefined; + if (secret && secret.length > 200) { + return c.text("Bad Request: hub.secret must be under 200 bytes", 400); + } const rawLease = parseInt(form.get("hub.lease_seconds") ?? "", 10); const leaseSeconds = isNaN(rawLease) ? DEFAULT_LEASE_SECONDS diff --git a/tsconfig.json b/tsconfig.json index ee87a84..27839f5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,11 @@ { "compilerOptions": { - "target": "ES2020", + "target": "ES2021", "module": "ESNext", "moduleResolution": "node", "esModuleInterop": true, "strict": true, - "lib": ["ES2020"], + "lib": ["ES2021"], "types": ["@cloudflare/workers-types"], "outDir": "dist", "noEmit": true,