Skip to content

Commit 710bb5d

Browse files
authored
fix: disallow localhost webhooks (#368)
* fix: disallow localhost webhooks * allow http but disallow localhost
1 parent 5a1c7ce commit 710bb5d

File tree

3 files changed

+51
-25
lines changed

3 files changed

+51
-25
lines changed

src/server/routes/webhooks/create.ts

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,83 +4,77 @@ import { FastifyInstance } from "fastify";
44
import { StatusCodes } from "http-status-codes";
55
import { insertWebhook } from "../../../db/webhooks/createWebhook";
66
import { WebhooksEventTypes } from "../../../schema/webhooks";
7+
import { isLocalhost } from "../../../utils/url";
78
import { standardResponseSchema } from "../../schemas/sharedApiSchemas";
89

910
const uriFormat = TypeSystem.Format("uri", (input: string) => {
11+
// Assert valid URL.
1012
try {
11-
if (input.startsWith("http://localhost")) return true;
12-
13-
const url = new URL(input);
14-
15-
if (url.protocol === "http:") {
16-
return false;
17-
}
18-
return true;
13+
new URL(input);
1914
} catch (err) {
2015
return false;
2116
}
17+
18+
return !isLocalhost(input);
2219
});
2320

2421
const BodySchema = Type.Object({
2522
url: Type.String({
26-
description: "URL to send the webhook to",
23+
description: "Webhook URL",
2724
format: uriFormat,
28-
examples: [
29-
"http://localhost:3000/webhooks",
30-
"https://example.com/webhooks",
31-
],
25+
examples: ["https://example.com/webhooks"],
3226
}),
3327
name: Type.Optional(
3428
Type.String({
35-
minLength: 5,
29+
minLength: 3,
3630
}),
3731
),
3832
eventType: Type.Enum(WebhooksEventTypes),
3933
});
4034

4135
BodySchema.examples = [
4236
{
43-
url: "http://localhost:3000/allTxUpdate",
37+
url: "https://example.com/allTxUpdate",
4438
name: "All Transaction Events",
4539
eventType: WebhooksEventTypes.ALL_TX,
4640
},
4741
{
48-
url: "http://localhost:3000/queuedTx",
42+
url: "https://example.com/queuedTx",
4943
name: "QueuedTx",
5044
eventType: WebhooksEventTypes.QUEUED_TX,
5145
},
5246
{
53-
url: "http://localhost:3000/retiredTx",
47+
url: "https://example.com/retiredTx",
5448
name: "RetriedTx",
5549
eventType: WebhooksEventTypes.RETRIED_TX,
5650
},
5751
{
58-
url: "http://localhost:3000/sentTx",
52+
url: "https://example.com/sentTx",
5953
name: "Sent Transaction Event",
6054
eventType: WebhooksEventTypes.SENT_TX,
6155
},
6256
{
63-
url: "http://localhost:3000/minedTx",
57+
url: "https://example.com/minedTx",
6458
name: "Mined Transaction Event",
6559
eventType: WebhooksEventTypes.MINED_TX,
6660
},
6761
{
68-
url: "http://localhost:3000/erroredTx",
62+
url: "https://example.com/erroredTx",
6963
name: "Errored Transaction Event",
7064
eventType: WebhooksEventTypes.ERRORED_TX,
7165
},
7266
{
73-
url: "http://localhost:3000/cancelledTx",
67+
url: "https://example.com/cancelledTx",
7468
name: "Cancelled Transaction Event",
7569
eventType: WebhooksEventTypes.CANCELLED_TX,
7670
},
7771
{
78-
url: "http://localhost:3000/walletBalance",
72+
url: "https://example.com/walletBalance",
7973
name: "Backend Wallet Balance Event",
8074
eventType: WebhooksEventTypes.BACKEND_WALLET_BALANCE,
8175
},
8276
{
83-
url: "http://localhost:3000/auth",
77+
url: "https://example.com/auth",
8478
name: "Auth Check",
8579
eventType: WebhooksEventTypes.AUTH,
8680
},
@@ -104,8 +98,9 @@ export async function createWebhook(fastify: FastifyInstance) {
10498
method: "POST",
10599
url: "/webhooks/create",
106100
schema: {
107-
summary: "Create a new webhook",
108-
description: "Create a new webhook",
101+
summary: "Create a webhook",
102+
description:
103+
"Create a webhook to call when certain blockchain events occur.",
109104
tags: ["Webhooks"],
110105
operationId: "create",
111106
body: BodySchema,

src/tests/url.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { isLocalhost } from "../utils/url";
2+
3+
describe("isLocalhost function", () => {
4+
test("should return true for localhost URL", () => {
5+
const localhostUrl = "http://localhost:3000/path";
6+
expect(isLocalhost(localhostUrl)).toBe(true);
7+
});
8+
9+
test("should return true for 127.0.0.1 URL", () => {
10+
const ipUrl = "http://127.0.0.1:8080/path";
11+
expect(isLocalhost(ipUrl)).toBe(true);
12+
});
13+
14+
test("should return false for external URL", () => {
15+
const externalUrl = "http://example.com/path";
16+
expect(isLocalhost(externalUrl)).toBe(false);
17+
});
18+
19+
test("should return false for invalid URL", () => {
20+
const invalidUrl = "not_a_url";
21+
expect(isLocalhost(invalidUrl)).toBe(false);
22+
});
23+
});

src/utils/url.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export const isLocalhost = (url: string) => {
2+
try {
3+
const parsed = new URL(url);
4+
return parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
5+
} catch (err) {
6+
return false;
7+
}
8+
};

0 commit comments

Comments
 (0)