Skip to content

Commit

Permalink
Add domain whitelist validation to webhooks
Browse files Browse the repository at this point in the history
  • Loading branch information
peelar committed Jan 9, 2024
1 parent 465dcc7 commit a02ae98
Show file tree
Hide file tree
Showing 6 changed files with 38 additions and 1 deletion.
3 changes: 2 additions & 1 deletion src/lib/env.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
),
},

Expand Down
2 changes: 2 additions & 0 deletions src/pages/api/webhooks/transaction-cancelation-requested.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions src/pages/api/webhooks/transaction-initialize-session.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions src/pages/api/webhooks/transaction-process-session.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -58,6 +59,8 @@ export default transactionProcessSessionSyncWebhook.createHandler(
);

try {
validateDomainWhiteList(req, ALLOWED_DOMAIN);

const appConfig = await resolveAppConfigFromCtx({
authData,
appMetadata: ctx.payload.recipient?.privateMetadata ?? [],
Expand Down
3 changes: 3 additions & 0 deletions src/pages/api/webhooks/transaction-refund-requested.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -41,6 +42,8 @@ class WebhookResponseBuilder extends SynchronousWebhookResponseBuilder<Transacti
*/
export default transactionRefundRequestedSyncWebhook.createHandler(
async (req, res, { authData, ...ctx }) => {
validateDomainWhiteList(req, ALLOWED_DOMAIN);

const responseBuilder = new WebhookResponseBuilder(res);
logger.debug({ payload: ctx.payload }, "handler called");

Expand Down
25 changes: 25 additions & 0 deletions src/pages/api/webhooks/validateDomainWhiteList.ts
Original file line number Diff line number Diff line change
@@ -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`);
}
}

0 comments on commit a02ae98

Please sign in to comment.