From a02ae98703dea8e54a197250f6dd025f6ec0042e Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Tue, 9 Jan 2024 14:35:16 +0100 Subject: [PATCH] Add domain whitelist validation to webhooks --- src/lib/env.mjs | 3 ++- .../transaction-cancelation-requested.ts | 2 ++ .../transaction-initialize-session.ts | 3 +++ .../webhooks/transaction-process-session.ts | 3 +++ .../webhooks/transaction-refund-requested.ts | 3 +++ .../api/webhooks/validateDomainWhiteList.ts | 25 +++++++++++++++++++ 6 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 src/pages/api/webhooks/validateDomainWhiteList.ts diff --git a/src/lib/env.mjs b/src/lib/env.mjs index ffe1ace..fe4119e 100644 --- a/src/lib/env.mjs +++ b/src/lib/env.mjs @@ -56,8 +56,9 @@ export const env = createEnv({ AUTHORIZE_PAYMENT_FORM_URL: z .string() .min(1) + .optional() .describe( - "Payment form URL. This is the address your front-end UI is running on. Make sure it is on https. Otherwise the Accept Hosted form will not work.", + "Payment form URL. This is the address your front-end UI is running on. Make sure it is on https, otherwise the Accept Hosted form will not work. This is not needed if you are running your front-end UI on the same domain and sending referrerPolicy: 'strict-origin-when-cross-origin' header.", ), }, diff --git a/src/pages/api/webhooks/transaction-cancelation-requested.ts b/src/pages/api/webhooks/transaction-cancelation-requested.ts index 26bd09c..630227f 100644 --- a/src/pages/api/webhooks/transaction-cancelation-requested.ts +++ b/src/pages/api/webhooks/transaction-cancelation-requested.ts @@ -1,5 +1,6 @@ import { SaleorSyncWebhook } from "@saleor/app-sdk/handlers/next"; import * as Sentry from "@sentry/nextjs"; +import { validateDomainWhiteList, ALLOWED_DOMAIN } from "./validateDomainWhiteList"; import { createLogger } from "@/lib/logger"; import { SynchronousWebhookResponseBuilder } from "@/lib/webhook-response-builder"; import { TransactionCancelationRequestedError } from "@/modules/webhooks/transaction-cancelation-requested"; @@ -45,6 +46,7 @@ export default transactionCancelationRequestedSyncWebhook.createHandler( logger.debug({ payload: ctx.payload }, "handler called"); try { + validateDomainWhiteList(req, ALLOWED_DOMAIN); const channelSlug = ctx.payload.transaction?.sourceObject?.channel.slug ?? ""; const appConfig = await resolveAppConfigFromCtx({ authData, diff --git a/src/pages/api/webhooks/transaction-initialize-session.ts b/src/pages/api/webhooks/transaction-initialize-session.ts index f9b1f4a..0892906 100644 --- a/src/pages/api/webhooks/transaction-initialize-session.ts +++ b/src/pages/api/webhooks/transaction-initialize-session.ts @@ -1,5 +1,6 @@ import { SaleorSyncWebhook } from "@saleor/app-sdk/handlers/next"; import * as Sentry from "@sentry/nextjs"; +import { validateDomainWhiteList, ALLOWED_DOMAIN } from "./validateDomainWhiteList"; import { normalizeError } from "@/errors"; import { createLogger } from "@/lib/logger"; import { SynchronousWebhookResponseBuilder } from "@/lib/webhook-response-builder"; @@ -48,6 +49,8 @@ export default transactionInitializeSessionSyncWebhook.createHandler( logger.info({ action: ctx.payload.action }, "called with:"); try { + validateDomainWhiteList(req, ALLOWED_DOMAIN); + const channelSlug = ctx.payload.sourceObject.channel.slug; const appConfig = await resolveAppConfigFromCtx({ authData, diff --git a/src/pages/api/webhooks/transaction-process-session.ts b/src/pages/api/webhooks/transaction-process-session.ts index 7052164..c035a43 100644 --- a/src/pages/api/webhooks/transaction-process-session.ts +++ b/src/pages/api/webhooks/transaction-process-session.ts @@ -1,5 +1,6 @@ import { SaleorSyncWebhook } from "@saleor/app-sdk/handlers/next"; import * as Sentry from "@sentry/nextjs"; +import { validateDomainWhiteList, ALLOWED_DOMAIN } from "./validateDomainWhiteList"; import { AuthorizeWebhookManager } from "@/modules/authorize-net/webhook/authorize-net-webhook-manager"; import { createLogger } from "@/lib/logger"; import { SynchronousWebhookResponseBuilder } from "@/lib/webhook-response-builder"; @@ -58,6 +59,8 @@ export default transactionProcessSessionSyncWebhook.createHandler( ); try { + validateDomainWhiteList(req, ALLOWED_DOMAIN); + const appConfig = await resolveAppConfigFromCtx({ authData, appMetadata: ctx.payload.recipient?.privateMetadata ?? [], diff --git a/src/pages/api/webhooks/transaction-refund-requested.ts b/src/pages/api/webhooks/transaction-refund-requested.ts index 1f284bf..180126e 100644 --- a/src/pages/api/webhooks/transaction-refund-requested.ts +++ b/src/pages/api/webhooks/transaction-refund-requested.ts @@ -1,5 +1,6 @@ import { SaleorSyncWebhook } from "@saleor/app-sdk/handlers/next"; import * as Sentry from "@sentry/nextjs"; +import { validateDomainWhiteList, ALLOWED_DOMAIN } from "./validateDomainWhiteList"; import { createLogger } from "@/lib/logger"; import { SynchronousWebhookResponseBuilder } from "@/lib/webhook-response-builder"; import { TransactionRefundRequestedError } from "@/modules/webhooks/transaction-refund-requested"; @@ -41,6 +42,8 @@ class WebhookResponseBuilder extends SynchronousWebhookResponseBuilder { + validateDomainWhiteList(req, ALLOWED_DOMAIN); + const responseBuilder = new WebhookResponseBuilder(res); logger.debug({ payload: ctx.payload }, "handler called"); diff --git a/src/pages/api/webhooks/validateDomainWhiteList.ts b/src/pages/api/webhooks/validateDomainWhiteList.ts new file mode 100644 index 0000000..a7b582a --- /dev/null +++ b/src/pages/api/webhooks/validateDomainWhiteList.ts @@ -0,0 +1,25 @@ +import { type NextApiRequest } from "next"; +import { BaseError } from "@/errors"; +import { env } from "@/lib/env.mjs"; + +const InvalidDomainError = BaseError.subclass("InvalidDomainError"); + +const MissingRefererError = BaseError.subclass("MissingRefererError"); + +export const ALLOWED_DOMAIN = ""; // todo: replace with domain from env config + +export function validateDomainWhiteList(request: NextApiRequest, whiteListedDomain: string) { + const referer = request.headers.referer ?? env.AUTHORIZE_PAYMENT_FORM_URL; + + if (!referer) { + throw new MissingRefererError( + "Referer not found on the request. Make sure the request is coming from the same domain and 'strict-origin-when-cross-origin' referrerPolicy is set.", + ); + } + + const isOriginAllowed = whiteListedDomain.includes(origin); + + if (!isOriginAllowed) { + throw new InvalidDomainError(`Origin ${origin} is not allowed`); + } +}