Skip to content

Commit 742e590

Browse files
authored
chore: Require HTTPS webhook URLs (#745)
* chore: Require HTTPS webhook URLs * fix build * allow localhost
1 parent 8c12183 commit 742e590

File tree

6 files changed

+59
-51
lines changed

6 files changed

+59
-51
lines changed

src/server/routes/contract/subscriptions/addContractSubscription.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
} from "../../../schemas/contractSubscription";
2121
import { standardResponseSchema } from "../../../schemas/sharedApiSchemas";
2222
import { getChainIdFromChain } from "../../../utils/chain";
23-
import { isValidHttpUrl } from "../../../utils/validator";
23+
import { isValidWebhookUrl } from "../../../utils/validator";
2424

2525
const bodySchema = Type.Object({
2626
chain: chainIdOrSlugSchema,
@@ -140,7 +140,7 @@ export async function addContractSubscription(fastify: FastifyInstance) {
140140
// Create the webhook (if provided).
141141
let webhookId: number | undefined;
142142
if (webhookUrl) {
143-
if (!isValidHttpUrl(webhookUrl)) {
143+
if (!isValidWebhookUrl(webhookUrl)) {
144144
throw createCustomError(
145145
"Invalid webhook URL. Make sure it starts with 'https://'.",
146146
StatusCodes.BAD_REQUEST,

src/server/routes/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ import { pageEventLogs } from "./contract/events/paginateEventLogs";
5959
import { accountRoutes } from "./contract/extensions/account";
6060
import { accountFactoryRoutes } from "./contract/extensions/accountFactory";
6161
import { erc1155Routes } from "./contract/extensions/erc1155";
62-
import { erc20Routes } from "./contract/extensions/erc20/index";
62+
import { erc20Routes } from "./contract/extensions/erc20";
6363
import { erc721Routes } from "./contract/extensions/erc721";
6464
import { marketplaceV3Routes } from "./contract/extensions/marketplaceV3/index";
6565
import { getABI } from "./contract/metadata/abi";
@@ -105,7 +105,7 @@ import { retryTransaction } from "./transaction/retry";
105105
import { retryFailedTransaction } from "./transaction/retry-failed";
106106
import { checkTxStatus } from "./transaction/status";
107107
import { syncRetryTransaction } from "./transaction/syncRetry";
108-
import { createWebhook } from "./webhooks/create";
108+
import { createWebhookRoute } from "./webhooks/create";
109109
import { getWebhooksEventTypes } from "./webhooks/events";
110110
import { getAllWebhooksData } from "./webhooks/getAll";
111111
import { revokeWebhook } from "./webhooks/revoke";
@@ -155,7 +155,7 @@ export const withRoutes = async (fastify: FastifyInstance) => {
155155

156156
// Webhooks
157157
await fastify.register(getAllWebhooksData);
158-
await fastify.register(createWebhook);
158+
await fastify.register(createWebhookRoute);
159159
await fastify.register(revokeWebhook);
160160
await fastify.register(getWebhooksEventTypes);
161161

src/server/routes/webhooks/create.ts

Lines changed: 11 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import { WebhooksEventTypes } from "../../../schema/webhooks";
66
import { createCustomError } from "../../middleware/error";
77
import { standardResponseSchema } from "../../schemas/sharedApiSchemas";
88
import { WebhookSchema, toWebhookSchema } from "../../schemas/webhook";
9-
import { isValidHttpUrl } from "../../utils/validator";
9+
import { isValidWebhookUrl } from "../../utils/validator";
1010

1111
const requestBodySchema = Type.Object({
1212
url: Type.String({
13-
description: "Webhook URL",
13+
description: "Webhook URL. Non-HTTPS URLs are not supported.",
1414
examples: ["https://example.com/webhook"],
1515
}),
1616
name: Type.Optional(
@@ -23,52 +23,21 @@ const requestBodySchema = Type.Object({
2323

2424
requestBodySchema.examples = [
2525
{
26-
url: "https://example.com/allTxUpdate",
27-
name: "All Transaction Events",
26+
url: "https://example.com/webhook",
27+
name: "Notify of transaction updates",
28+
secret: "...",
2829
eventType: WebhooksEventTypes.ALL_TX,
29-
},
30-
{
31-
url: "https://example.com/queuedTx",
32-
name: "QueuedTx",
33-
eventType: WebhooksEventTypes.QUEUED_TX,
34-
},
35-
{
36-
url: "https://example.com/sentTx",
37-
name: "Sent Transaction Event",
38-
eventType: WebhooksEventTypes.SENT_TX,
39-
},
40-
{
41-
url: "https://example.com/minedTx",
42-
name: "Mined Transaction Event",
43-
eventType: WebhooksEventTypes.MINED_TX,
44-
},
45-
{
46-
url: "https://example.com/erroredTx",
47-
name: "Errored Transaction Event",
48-
eventType: WebhooksEventTypes.ERRORED_TX,
49-
},
50-
{
51-
url: "https://example.com/cancelledTx",
52-
name: "Cancelled Transaction Event",
53-
eventType: WebhooksEventTypes.CANCELLED_TX,
54-
},
55-
{
56-
url: "https://example.com/walletBalance",
57-
name: "Backend Wallet Balance Event",
58-
eventType: WebhooksEventTypes.BACKEND_WALLET_BALANCE,
59-
},
60-
{
61-
url: "https://example.com/auth",
62-
name: "Auth Check",
63-
eventType: WebhooksEventTypes.AUTH,
30+
active: true,
31+
createdAt: "2024-10-02T02:07:27.255Z",
32+
id: 42,
6433
},
6534
];
6635

6736
const responseBodySchema = Type.Object({
6837
result: WebhookSchema,
6938
});
7039

71-
export async function createWebhook(fastify: FastifyInstance) {
40+
export async function createWebhookRoute(fastify: FastifyInstance) {
7241
fastify.route<{
7342
Body: Static<typeof requestBodySchema>;
7443
Reply: Static<typeof responseBodySchema>;
@@ -78,7 +47,7 @@ export async function createWebhook(fastify: FastifyInstance) {
7847
schema: {
7948
summary: "Create a webhook",
8049
description:
81-
"Create a webhook to call when certain blockchain events occur.",
50+
"Create a webhook to call when a specific Engine event occurs.",
8251
tags: ["Webhooks"],
8352
operationId: "createWebhook",
8453
body: requestBodySchema,
@@ -90,7 +59,7 @@ export async function createWebhook(fastify: FastifyInstance) {
9059
handler: async (req, res) => {
9160
const { url, name, eventType } = req.body;
9261

93-
if (!isValidHttpUrl(url)) {
62+
if (!isValidWebhookUrl(url)) {
9463
throw createCustomError(
9564
"Invalid webhook URL. Make sure it starts with 'https://'.",
9665
StatusCodes.BAD_REQUEST,

src/server/schemas/webhook.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,6 @@ export const toWebhookSchema = (
1919
eventType: webhook.eventType,
2020
secret: webhook.secret,
2121
createdAt: webhook.createdAt.toISOString(),
22-
active: webhook.revokedAt ? false : true,
22+
active: !webhook.revokedAt,
2323
id: webhook.id,
2424
});

src/server/utils/validator.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,15 @@ export const checkAndReturnNFTSignaturePayload = <
9090
return updatedPayload;
9191
};
9292

93-
export const isValidHttpUrl = (urlString: string): boolean => {
93+
export const isValidWebhookUrl = (input: string): boolean => {
9494
try {
95-
const url = new URL(urlString);
96-
return url.protocol === "http:" || url.protocol === "https:";
95+
const url = new URL(input);
96+
return (
97+
url.protocol === "https:" ||
98+
// Allow http for localhost only.
99+
(url.protocol === "http:" &&
100+
["localhost", "0.0.0.0", "127.0.0.1"].includes(url.hostname))
101+
);
97102
} catch {
98103
return false;
99104
}

src/tests/validator.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { describe, expect, it } from "vitest";
2+
import { isValidWebhookUrl } from "../server/utils/validator";
3+
4+
describe("isValidWebhookUrl", () => {
5+
it("should return true for a valid HTTPS URL", () => {
6+
expect(isValidWebhookUrl("https://example.com")).toBe(true);
7+
});
8+
9+
it("should return false for an HTTP URL", () => {
10+
expect(isValidWebhookUrl("http://example.com")).toBe(false);
11+
});
12+
13+
it("should return false for a URL without protocol", () => {
14+
expect(isValidWebhookUrl("example.com")).toBe(false);
15+
});
16+
17+
it("should return false for an invalid URL", () => {
18+
expect(isValidWebhookUrl("invalid-url")).toBe(false);
19+
});
20+
21+
it("should return false for a URL with a different protocol", () => {
22+
expect(isValidWebhookUrl("ftp://example.com")).toBe(false);
23+
});
24+
25+
it("should return false for an empty string", () => {
26+
expect(isValidWebhookUrl("")).toBe(false);
27+
});
28+
29+
it("should return true for a http localhost", () => {
30+
expect(isValidWebhookUrl("http://localhost:3000")).toBe(true);
31+
expect(isValidWebhookUrl("http://0.0.0.0:3000")).toBe(true);
32+
expect(isValidWebhookUrl("http://user:pass@127.0.0.1:3000")).toBe(true);
33+
});
34+
});

0 commit comments

Comments
 (0)