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 fb151bf..4eb50db 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"; @@ -46,6 +47,7 @@ export default transactionCancelationRequestedSyncWebhook.createHandler( try { const authorizeConfig = getAuthorizeConfig(); + validateDomainWhiteList(req, ALLOWED_DOMAIN); const authorizeWebhookManager = new AuthorizeWebhookManager({ appConfig: authorizeConfig, }); diff --git a/src/pages/api/webhooks/transaction-initialize-session.ts b/src/pages/api/webhooks/transaction-initialize-session.ts index 86a6f6d..0c051d7 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( try { const authorizeConfig = getAuthorizeConfig(); + validateDomainWhiteList(req, ALLOWED_DOMAIN); + const authorizeWebhookManager = new AuthorizeWebhookManager({ appConfig: authorizeConfig, }); diff --git a/src/pages/api/webhooks/transaction-process-session.ts b/src/pages/api/webhooks/transaction-process-session.ts index e014360..97c5c3c 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 { const authorizeConfig = getAuthorizeConfig(); + validateDomainWhiteList(req, ALLOWED_DOMAIN); + const authorizeWebhookManager = new AuthorizeWebhookManager({ appConfig: authorizeConfig, }); diff --git a/src/pages/api/webhooks/transaction-refund-requested.ts b/src/pages/api/webhooks/transaction-refund-requested.ts index eeda685..fda38ec 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"; @@ -40,6 +41,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`); + } +}