diff --git a/plugins/webhooks/client/components/WebhookSubscriptionForm.tsx b/plugins/webhooks/client/components/WebhookSubscriptionForm.tsx
index f79af68ecc..61d0fc24fd 100644
--- a/plugins/webhooks/client/components/WebhookSubscriptionForm.tsx
+++ b/plugins/webhooks/client/components/WebhookSubscriptionForm.tsx
@@ -13,6 +13,7 @@ import Input from "~/components/Input";
import Text from "~/components/Text";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useMobile from "~/hooks/useMobile";
+import isCloudHosted from "~/utils/isCloudHosted";
import Flex from "@shared/components/Flex";
const WEBHOOK_EVENTS = {
@@ -153,6 +154,9 @@ function WebhookSubscriptionForm({ handleSubmit, webhookSubscription }: Props) {
});
const events = watch("events");
+ const url = watch("url");
+ const showInsecureUrlWarning =
+ !isCloudHosted && typeof url === "string" && url.startsWith("http://");
const selectedGroups = filter(events, (e) => !e.includes("."));
const isAllEventSelected = includes(events, "*");
const filteredEvents = filter(events, (e) => {
@@ -224,9 +228,16 @@ function WebhookSubscriptionForm({ handleSubmit, webhookSubscription }: Props) {
!env.isCloudHosted || val.startsWith("https://"), {
+ error: "Webhook url must use https",
+ });
+
export const WebhookSubscriptionsListSchema = BaseSchema.extend({
body: z.object({
/** Webhook subscriptions sorting direction */
@@ -33,7 +40,7 @@ export type WebhookSubscriptionsListReq = z.infer<
export const WebhookSubscriptionsCreateSchema = z.object({
body: z.object({
name: z.string(),
- url: z.url(),
+ url: webhookUrl,
secret: z.string().optional(),
events: z.array(z.string()),
}),
@@ -47,7 +54,7 @@ export const WebhookSubscriptionsUpdateSchema = z.object({
body: z.object({
id: z.uuid(),
name: z.string(),
- url: z.url(),
+ url: webhookUrl,
secret: z.string().optional(),
events: z.array(z.string()),
}),
diff --git a/plugins/webhooks/server/api/webhookSubscriptions.test.ts b/plugins/webhooks/server/api/webhookSubscriptions.test.ts
index 1eaf1d28de..7d3cf3783e 100644
--- a/plugins/webhooks/server/api/webhookSubscriptions.test.ts
+++ b/plugins/webhooks/server/api/webhookSubscriptions.test.ts
@@ -1,3 +1,4 @@
+import env from "@server/env";
import {
buildAdmin,
buildUser,
@@ -167,6 +168,39 @@ describe("#webhookSubscriptions.create", () => {
expect(webhook.secret).toEqual(secret);
expect(webhook.enabled).toEqual(true);
});
+
+ it("should reject http urls when cloud hosted", async () => {
+ vi.spyOn(env, "isCloudHosted", "get").mockReturnValue(true);
+
+ const user = await buildAdmin();
+ const res = await server.post("/api/webhookSubscriptions.create", user, {
+ body: {
+ name: "Test webhook",
+ url: "http://www.example.com",
+ events: ["comments"],
+ },
+ });
+
+ expect(res.status).toEqual(400);
+ });
+
+ it("should allow http urls when not cloud hosted", async () => {
+ vi.spyOn(env, "isCloudHosted", "get").mockReturnValue(false);
+
+ const user = await buildAdmin();
+ const url = "http://www.example.com";
+ const res = await server.post("/api/webhookSubscriptions.create", user, {
+ body: {
+ name: "Test webhook",
+ url,
+ events: ["comments"],
+ },
+ });
+ const body = await res.json();
+
+ expect(res.status).toEqual(200);
+ expect(body.data.url).toEqual(url);
+ });
});
describe("#webhookSubscriptions.update", () => {
diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json
index 483f33162b..09e6ba9493 100644
--- a/shared/i18n/locales/en_US/translation.json
+++ b/shared/i18n/locales/en_US/translation.json
@@ -1610,6 +1610,7 @@
"Provide a descriptive name for this webhook and the URL we should send a POST request to when matching events are created.": "Provide a descriptive name for this webhook and the URL we should send a POST request to when matching events are created.",
"A memorable identifer": "A memorable identifer",
"URL": "URL",
+ "Webhook delivery over http is insecure, use https if possible": "Webhook delivery over http is insecure, use https if possible",
"Signing secret": "Signing secret",
"Subscribe to all events, groups, or individual events. We recommend only subscribing to the minimum amount of events that your application needs to function.": "Subscribe to all events, groups, or individual events. We recommend only subscribing to the minimum amount of events that your application needs to function.",
"All events": "All events",