From a977192105f8dfa70fe0fee02acbfeb59c1ff389 Mon Sep 17 00:00:00 2001 From: Kiran K Date: Mon, 13 Jan 2025 11:19:10 +0530 Subject: [PATCH] Refactor QStash signature verification to use raw request body - Updated multiple API routes to read the raw body from requests instead of parsing JSON directly. - Modified the `verifyQstashSignature` function to accept the raw body for signature verification. - Ensured consistent handling of request bodies across various endpoints, improving reliability in signature validation. --- .../api/cron/domains/configure-dns/route.ts | 7 +++--- apps/web/app/api/cron/domains/delete/route.ts | 7 +++--- .../app/api/cron/domains/transfer/route.ts | 9 ++++---- apps/web/app/api/cron/domains/update/route.ts | 9 ++++---- apps/web/app/api/cron/import/bitly/route.ts | 6 +++-- apps/web/app/api/cron/import/csv/route.ts | 6 +++-- .../app/api/cron/import/rebrandly/route.ts | 6 +++-- apps/web/app/api/cron/import/short/route.ts | 6 +++-- apps/web/app/api/cron/links/delete/route.ts | 7 +++--- .../app/api/cron/shopify/order-paid/route.ts | 6 ++--- apps/web/app/api/cron/usage/route.ts | 5 +++- .../app/api/cron/workspaces/delete/route.ts | 7 +++--- apps/web/lib/cron/verify-qstash.ts | 23 +++++++++++-------- 13 files changed, 60 insertions(+), 44 deletions(-) diff --git a/apps/web/app/api/cron/domains/configure-dns/route.ts b/apps/web/app/api/cron/domains/configure-dns/route.ts index aa3af1a844..2ba8d813bb 100644 --- a/apps/web/app/api/cron/domains/configure-dns/route.ts +++ b/apps/web/app/api/cron/domains/configure-dns/route.ts @@ -9,9 +9,10 @@ export const dynamic = "force-dynamic"; */ export async function POST(req: Request) { try { - const body = await req.json(); - await verifyQstashSignature({ req, body }); - const { domain } = body; + const rawBody = await req.text(); + await verifyQstashSignature({ req, rawBody }); + + const { domain } = JSON.parse(rawBody); const res = await configureDNS({ domain }); console.log("Dynadot DNS configured.", res); diff --git a/apps/web/app/api/cron/domains/delete/route.ts b/apps/web/app/api/cron/domains/delete/route.ts index bf9a733823..28baee2c7d 100644 --- a/apps/web/app/api/cron/domains/delete/route.ts +++ b/apps/web/app/api/cron/domains/delete/route.ts @@ -18,11 +18,10 @@ const schema = z.object({ // POST /api/cron/domains/delete export async function POST(req: Request) { try { - const body = await req.json(); + const rawBody = await req.text(); + await verifyQstashSignature({ req, rawBody }); - await verifyQstashSignature({ req, body }); - - const { domain, workspaceId } = schema.parse(body); + const { domain, workspaceId } = schema.parse(JSON.parse(rawBody)); const domainRecord = await prisma.domain.findUnique({ where: { diff --git a/apps/web/app/api/cron/domains/transfer/route.ts b/apps/web/app/api/cron/domains/transfer/route.ts index 671afbec05..a54a5f8af7 100644 --- a/apps/web/app/api/cron/domains/transfer/route.ts +++ b/apps/web/app/api/cron/domains/transfer/route.ts @@ -19,11 +19,12 @@ export const dynamic = "force-dynamic"; export async function POST(req: Request) { try { - const body = await req.json(); + const rawBody = await req.text(); + await verifyQstashSignature({ req, rawBody }); - await verifyQstashSignature({ req, body }); - - const { currentWorkspaceId, newWorkspaceId, domain } = schema.parse(body); + const { currentWorkspaceId, newWorkspaceId, domain } = schema.parse( + JSON.parse(rawBody), + ); const links = await prisma.link.findMany({ where: { domain, projectId: currentWorkspaceId }, diff --git a/apps/web/app/api/cron/domains/update/route.ts b/apps/web/app/api/cron/domains/update/route.ts index d294002cec..3054a2b26d 100644 --- a/apps/web/app/api/cron/domains/update/route.ts +++ b/apps/web/app/api/cron/domains/update/route.ts @@ -20,11 +20,12 @@ const pageSize = 100; // POST /api/cron/domains/update export async function POST(req: Request) { try { - const body = await req.json(); + const rawBody = await req.text(); + await verifyQstashSignature({ req, rawBody }); - await verifyQstashSignature({ req, body }); - - const { newDomain, oldDomain, workspaceId, page } = schema.parse(body); + const { newDomain, oldDomain, workspaceId, page } = schema.parse( + JSON.parse(rawBody), + ); const newDomainRecord = await prisma.domain.findUnique({ where: { diff --git a/apps/web/app/api/cron/import/bitly/route.ts b/apps/web/app/api/cron/import/bitly/route.ts index 32b27df3af..d2d27f485e 100644 --- a/apps/web/app/api/cron/import/bitly/route.ts +++ b/apps/web/app/api/cron/import/bitly/route.ts @@ -12,8 +12,10 @@ export const dynamic = "force-dynamic"; export async function POST(req: Request) { try { - const body = await req.json(); - await verifyQstashSignature({ req, body }); + const rawBody = await req.text(); + await verifyQstashSignature({ req, rawBody }); + + const body = JSON.parse(rawBody); const { workspaceId, bitlyGroup, importTags } = body; try { diff --git a/apps/web/app/api/cron/import/csv/route.ts b/apps/web/app/api/cron/import/csv/route.ts index 8490167385..9a45dd3ef3 100644 --- a/apps/web/app/api/cron/import/csv/route.ts +++ b/apps/web/app/api/cron/import/csv/route.ts @@ -45,8 +45,10 @@ type MapperResult = export async function POST(req: Request) { try { - const body = await req.json(); - await verifyQstashSignature({ req, body }); + const rawBody = await req.text(); + await verifyQstashSignature({ req, rawBody }); + + const body = JSON.parse(rawBody); const { workspaceId, userId, id, url } = body; const mapping = linkMappingSchema.parse(body.mapping); diff --git a/apps/web/app/api/cron/import/rebrandly/route.ts b/apps/web/app/api/cron/import/rebrandly/route.ts index b487dce082..fc913ab32a 100644 --- a/apps/web/app/api/cron/import/rebrandly/route.ts +++ b/apps/web/app/api/cron/import/rebrandly/route.ts @@ -10,8 +10,10 @@ export const dynamic = "force-dynamic"; export async function POST(req: Request) { try { - const body = await req.json(); - await verifyQstashSignature({ req, body }); + const rawBody = await req.text(); + await verifyQstashSignature({ req, rawBody }); + + const body = JSON.parse(rawBody); const { workspaceId, importTags } = body; try { diff --git a/apps/web/app/api/cron/import/short/route.ts b/apps/web/app/api/cron/import/short/route.ts index a3cc91012c..332d254388 100644 --- a/apps/web/app/api/cron/import/short/route.ts +++ b/apps/web/app/api/cron/import/short/route.ts @@ -10,8 +10,10 @@ export const dynamic = "force-dynamic"; export async function POST(req: Request) { try { - const body = await req.json(); - await verifyQstashSignature({ req, body }); + const rawBody = await req.text(); + await verifyQstashSignature({ req, rawBody }); + + const body = JSON.parse(rawBody); const { workspaceId, userId, diff --git a/apps/web/app/api/cron/links/delete/route.ts b/apps/web/app/api/cron/links/delete/route.ts index 4dfcfb3dbd..d53637f4cc 100644 --- a/apps/web/app/api/cron/links/delete/route.ts +++ b/apps/web/app/api/cron/links/delete/route.ts @@ -11,9 +11,10 @@ export const dynamic = "force-dynamic"; */ export async function POST(req: Request) { try { - const body = await req.json(); - await verifyQstashSignature({ req, body }); - const { linkId } = body; + const rawBody = await req.text(); + await verifyQstashSignature({ req, rawBody }); + + const { linkId } = JSON.parse(rawBody); const link = await prisma.link.findUnique({ where: { diff --git a/apps/web/app/api/cron/shopify/order-paid/route.ts b/apps/web/app/api/cron/shopify/order-paid/route.ts index 47cf001fd5..9786ff4c4c 100644 --- a/apps/web/app/api/cron/shopify/order-paid/route.ts +++ b/apps/web/app/api/cron/shopify/order-paid/route.ts @@ -14,10 +14,10 @@ const schema = z.object({ // POST /api/cron/shopify/order-paid export async function POST(req: Request) { try { - const body = await req.json(); - await verifyQstashSignature({ req, body }); + const rawBody = await req.text(); + await verifyQstashSignature({ req, rawBody }); - const { workspaceId, checkoutToken } = schema.parse(body); + const { workspaceId, checkoutToken } = schema.parse(JSON.parse(rawBody)); // Find Shopify order const event = await redis.hget( diff --git a/apps/web/app/api/cron/usage/route.ts b/apps/web/app/api/cron/usage/route.ts index ba337e5ee9..816a1df04a 100644 --- a/apps/web/app/api/cron/usage/route.ts +++ b/apps/web/app/api/cron/usage/route.ts @@ -16,7 +16,10 @@ async function handler(req: Request) { if (req.method === "GET") { await verifyVercelSignature(req); } else if (req.method === "POST") { - await verifyQstashSignature({ req }); + await verifyQstashSignature({ + req, + rawBody: await req.text(), + }); } await updateUsage(); diff --git a/apps/web/app/api/cron/workspaces/delete/route.ts b/apps/web/app/api/cron/workspaces/delete/route.ts index 00ad3c6e32..ec101cdf02 100644 --- a/apps/web/app/api/cron/workspaces/delete/route.ts +++ b/apps/web/app/api/cron/workspaces/delete/route.ts @@ -15,11 +15,10 @@ const schema = z.object({ // POST /api/cron/workspaces/delete export async function POST(req: Request) { try { - const body = await req.json(); + const rawBody = await req.text(); + await verifyQstashSignature({ req, rawBody }); - await verifyQstashSignature({ req, body }); - - const { workspaceId } = schema.parse(body); + const { workspaceId } = schema.parse(JSON.parse(rawBody)); const workspace = await prisma.project.findUnique({ where: { diff --git a/apps/web/lib/cron/verify-qstash.ts b/apps/web/lib/cron/verify-qstash.ts index 6ff59d605f..64f3a5efeb 100644 --- a/apps/web/lib/cron/verify-qstash.ts +++ b/apps/web/lib/cron/verify-qstash.ts @@ -9,26 +9,29 @@ const receiver = new Receiver({ export const verifyQstashSignature = async ({ req, - body, - bodyType = "json", + rawBody, }: { req: Request; - body?: any; - // due to a weird QStash bug, webhook URLs that have query params - // need to be verified with the text body type (instead of JSON) - bodyType?: "json" | "text"; + rawBody: string; // Make sure to pass the raw body not the parsed JSON }) => { - body = body || (bodyType === "json" ? await req.json() : await req.text()); + const signature = req.headers.get("Upstash-Signature"); + + if (!signature) { + throw new DubApiError({ + code: "bad_request", + message: "Upstash-Signature header not found.", + }); + } const isValid = await receiver.verify({ - signature: req.headers.get("Upstash-Signature") || "", - body: bodyType === "json" ? JSON.stringify(body) : body, + signature, + body: rawBody, }); if (!isValid) { throw new DubApiError({ code: "unauthorized", - message: "Invalid QStash request signature", + message: "Invalid QStash request signature.", }); } };