diff --git a/app/server/modules/notifications/__tests__/notification-target-policy.test.ts b/app/server/modules/notifications/__tests__/notification-target-policy.test.ts index 5d2d611d..59fb7ac9 100644 --- a/app/server/modules/notifications/__tests__/notification-target-policy.test.ts +++ b/app/server/modules/notifications/__tests__/notification-target-policy.test.ts @@ -2,6 +2,30 @@ import { describe, expect, test } from "vitest"; import { assertNotificationTargetAllowed } from "../utils/notification-target-policy"; describe("assertNotificationTargetAllowed", () => { + test("requires email SMTP targets to match the allowlist", () => { + const notificationConfig = { + type: "email" as const, + smtpHost: "smtp.example.com", + smtpPort: 587, + from: "backups@example.com", + to: ["admin@example.com"], + useTLS: true, + }; + + expect(() => assertNotificationTargetAllowed(notificationConfig, [])).toThrow( + "Add smtp://smtp.example.com:587 to WEBHOOK_ALLOWED_ORIGINS", + ); + + expect(() => assertNotificationTargetAllowed(notificationConfig, ["smtp://smtp.example.com:587"])).not.toThrow(); + expect(() => assertNotificationTargetAllowed(notificationConfig, ["smtp://smtp.example.com:25"])).toThrow(); + }); + + test("rejects notification types that are not classified by the SSRF policy", () => { + expect(() => assertNotificationTargetAllowed({ type: "future-networked" } as never, [])).toThrow( + 'Unsupported notification type "future-networked" for the SSRF policy.', + ); + }); + test("allows fixed-provider custom Shoutrrr services without a network target allowlist", () => { expect(() => assertNotificationTargetAllowed({ type: "custom", shoutrrrUrl: "discord://token@webhook-id" }, []), diff --git a/app/server/modules/notifications/utils/notification-target-policy.ts b/app/server/modules/notifications/utils/notification-target-policy.ts index ffa2632c..c79137e0 100644 --- a/app/server/modules/notifications/utils/notification-target-policy.ts +++ b/app/server/modules/notifications/utils/notification-target-policy.ts @@ -115,6 +115,17 @@ const getCustomShoutrrrTarget = (shoutrrrUrl: string) => { const getNotificationTarget = (notificationConfig: NotificationConfig) => { switch (notificationConfig.type) { + case "email": { + const smtpTarget = new URL("smtp://placeholder"); + smtpTarget.hostname = notificationConfig.smtpHost; + smtpTarget.port = String(notificationConfig.smtpPort); + return `${smtpTarget.protocol}//${smtpTarget.host}`; + } + case "slack": + case "discord": + case "pushover": + case "telegram": + return null; case "generic": return notificationConfig.url; case "gotify": @@ -123,8 +134,12 @@ const getNotificationTarget = (notificationConfig: NotificationConfig) => { return notificationConfig.serverUrl ?? null; case "custom": return getCustomShoutrrrTarget(notificationConfig.shoutrrrUrl); - default: - return null; + default: { + const _exhaustive: never = notificationConfig; + throw new BadRequestError( + `Unsupported notification type "${(_exhaustive as NotificationConfig).type}" for the SSRF policy.`, + ); + } } };