From 710bb5dea3131134552a837b8072c8f6670969d0 Mon Sep 17 00:00:00 2001 From: Phillip Ho Date: Wed, 10 Jan 2024 08:00:31 +0900 Subject: [PATCH] fix: disallow localhost webhooks (#368) * fix: disallow localhost webhooks * allow http but disallow localhost --- src/server/routes/webhooks/create.ts | 45 +++++++++++++--------------- src/tests/url.test.ts | 23 ++++++++++++++ src/utils/url.ts | 8 +++++ 3 files changed, 51 insertions(+), 25 deletions(-) create mode 100644 src/tests/url.test.ts create mode 100644 src/utils/url.ts diff --git a/src/server/routes/webhooks/create.ts b/src/server/routes/webhooks/create.ts index 747a4e483..15fada81c 100644 --- a/src/server/routes/webhooks/create.ts +++ b/src/server/routes/webhooks/create.ts @@ -4,35 +4,29 @@ import { FastifyInstance } from "fastify"; import { StatusCodes } from "http-status-codes"; import { insertWebhook } from "../../../db/webhooks/createWebhook"; import { WebhooksEventTypes } from "../../../schema/webhooks"; +import { isLocalhost } from "../../../utils/url"; import { standardResponseSchema } from "../../schemas/sharedApiSchemas"; const uriFormat = TypeSystem.Format("uri", (input: string) => { + // Assert valid URL. try { - if (input.startsWith("http://localhost")) return true; - - const url = new URL(input); - - if (url.protocol === "http:") { - return false; - } - return true; + new URL(input); } catch (err) { return false; } + + return !isLocalhost(input); }); const BodySchema = Type.Object({ url: Type.String({ - description: "URL to send the webhook to", + description: "Webhook URL", format: uriFormat, - examples: [ - "http://localhost:3000/webhooks", - "https://example.com/webhooks", - ], + examples: ["https://example.com/webhooks"], }), name: Type.Optional( Type.String({ - minLength: 5, + minLength: 3, }), ), eventType: Type.Enum(WebhooksEventTypes), @@ -40,47 +34,47 @@ const BodySchema = Type.Object({ BodySchema.examples = [ { - url: "http://localhost:3000/allTxUpdate", + url: "https://example.com/allTxUpdate", name: "All Transaction Events", eventType: WebhooksEventTypes.ALL_TX, }, { - url: "http://localhost:3000/queuedTx", + url: "https://example.com/queuedTx", name: "QueuedTx", eventType: WebhooksEventTypes.QUEUED_TX, }, { - url: "http://localhost:3000/retiredTx", + url: "https://example.com/retiredTx", name: "RetriedTx", eventType: WebhooksEventTypes.RETRIED_TX, }, { - url: "http://localhost:3000/sentTx", + url: "https://example.com/sentTx", name: "Sent Transaction Event", eventType: WebhooksEventTypes.SENT_TX, }, { - url: "http://localhost:3000/minedTx", + url: "https://example.com/minedTx", name: "Mined Transaction Event", eventType: WebhooksEventTypes.MINED_TX, }, { - url: "http://localhost:3000/erroredTx", + url: "https://example.com/erroredTx", name: "Errored Transaction Event", eventType: WebhooksEventTypes.ERRORED_TX, }, { - url: "http://localhost:3000/cancelledTx", + url: "https://example.com/cancelledTx", name: "Cancelled Transaction Event", eventType: WebhooksEventTypes.CANCELLED_TX, }, { - url: "http://localhost:3000/walletBalance", + url: "https://example.com/walletBalance", name: "Backend Wallet Balance Event", eventType: WebhooksEventTypes.BACKEND_WALLET_BALANCE, }, { - url: "http://localhost:3000/auth", + url: "https://example.com/auth", name: "Auth Check", eventType: WebhooksEventTypes.AUTH, }, @@ -104,8 +98,9 @@ export async function createWebhook(fastify: FastifyInstance) { method: "POST", url: "/webhooks/create", schema: { - summary: "Create a new webhook", - description: "Create a new webhook", + summary: "Create a webhook", + description: + "Create a webhook to call when certain blockchain events occur.", tags: ["Webhooks"], operationId: "create", body: BodySchema, diff --git a/src/tests/url.test.ts b/src/tests/url.test.ts new file mode 100644 index 000000000..53f6bd3d5 --- /dev/null +++ b/src/tests/url.test.ts @@ -0,0 +1,23 @@ +import { isLocalhost } from "../utils/url"; + +describe("isLocalhost function", () => { + test("should return true for localhost URL", () => { + const localhostUrl = "http://localhost:3000/path"; + expect(isLocalhost(localhostUrl)).toBe(true); + }); + + test("should return true for 127.0.0.1 URL", () => { + const ipUrl = "http://127.0.0.1:8080/path"; + expect(isLocalhost(ipUrl)).toBe(true); + }); + + test("should return false for external URL", () => { + const externalUrl = "http://example.com/path"; + expect(isLocalhost(externalUrl)).toBe(false); + }); + + test("should return false for invalid URL", () => { + const invalidUrl = "not_a_url"; + expect(isLocalhost(invalidUrl)).toBe(false); + }); +}); diff --git a/src/utils/url.ts b/src/utils/url.ts new file mode 100644 index 000000000..9c4eefef1 --- /dev/null +++ b/src/utils/url.ts @@ -0,0 +1,8 @@ +export const isLocalhost = (url: string) => { + try { + const parsed = new URL(url); + return parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1"; + } catch (err) { + return false; + } +};