{"record":{"id":"45550d68e61728ee","repo":"koala73/worldmonitor","slug":"webhook-url-must-use-https","errorCode":null,"errorMessage":"Webhook URL must use HTTPS","messagePattern":"Webhook URL must use HTTPS","errorType":"validation","errorClass":"Error","httpStatus":400,"severity":"error","filePath":"api/_notification-webhook-ssrf.ts","lineNumber":223,"sourceCode":"async function defaultResolveHostname(hostname: string): Promise<string[]> {\n  const records = await Promise.all([\n    resolveDnsJson(hostname, 'A'),\n    resolveDnsJson(hostname, 'AAAA'),\n  ]);\n  return records.flat();\n}\n\n/**\n * Fail fast at registration when the webhook hostname currently resolves to a\n * private or reserved address. Delivery repeats this check (and pins its\n * connection) because DNS can change after registration.\n */\nexport async function assertNotificationWebhookRegistrationUrlSafe(\n  rawUrl: string,\n  resolveHostname: ResolveHostname = defaultResolveHostname,\n): Promise<void> {\n  const staticError = blockedNotificationWebhookUrlReason(rawUrl);\n  if (staticError) throw new Error(staticError);\n\n  const hostname = new URL(rawUrl).hostname.toLowerCase();\n  if (isIpLiteral(hostname)) return;\n  let resolvedAddresses: string[];\n  try {\n    resolvedAddresses = await resolveHostname(hostname);\n  } catch (error) {\n    const message = error instanceof Error ? error.message : String(error);\n    throw new Error(`Webhook URL DNS resolution failed: ${message}`);\n  }\n  if (!resolvedAddresses.length) throw new Error('Webhook URL DNS resolution returned no addresses');\n  if (resolvedAddresses.some(isBlockedNotificationResolvedAddress)) {\n    throw new Error('Webhook URL must not point to a private/local address');\n  }\n}\n","sourceCodeStart":205,"sourceCodeEnd":239,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/api/_notification-webhook-ssrf.ts#L205-L239","documentation":"Returned by blockedNotificationWebhookUrlReason when the parsed URL's protocol is anything other than `https:`. Webhook payloads can carry alert content, so the registration gate refuses cleartext http:// (and any other scheme) to prevent interception and to keep the SSRF surface consistent.","triggerScenarios":"A webhook registration URL whose scheme is `http:`, `ftp:`, `file:`, or any non-https protocol passes URL parsing but fails this check at api/_notification-webhook-ssrf.ts:169.","commonSituations":"Local dev webhook pointing at http://localhost; an internal/staging integration that has not been issued a TLS cert; a typo leaving the scheme off so the form defaults to http.","solutions":["Register an https:// URL — terminate TLS at the webhook target (Let's Encrypt, Caddy, or a platform-managed cert).","For local testing, expose the local server over TLS via a tunnel (e.g. ngrok/cloudflared with https) before registering.","Update the registration UI to force https and reject http submissions before they reach the API."],"exampleFix":"// before\nregisterWebhook('http://localhost:9000/hook')\n// after\nregisterWebhook('https://my-tunnel.example.dev/hook')","handlingStrategy":"validation","validationCode":"function isHttpsUrl(rawUrl: string): boolean {\n  try { return new URL(rawUrl).protocol === 'https:'; } catch { return false; }\n}\n\nif (!isHttpsUrl(input)) return res.status(400).json({ error: 'Webhook URL must use HTTPS.' });","typeGuard":"function isHttpsWebhookUrl(value: unknown): value is string {\n  if (typeof value !== 'string') return false;\n  try { return new URL(value).protocol === 'https:'; } catch { return false; }\n}","tryCatchPattern":"try {\n  await assertNotificationWebhookRegistrationUrlSafe(rawUrl);\n} catch (err) {\n  if (err.message === 'Webhook URL must use HTTPS') {\n    return res.status(400).json({ error: 'Webhook URL must use HTTPS. Use an https:// URL.' });\n  }\n  throw err;\n}","preventionTips":["Terminate TLS at the webhook target before registering it.","In the registration UI, reject any submitted value whose parsed protocol is not https.","For local dev, use an https tunnel (ngrok/cloudflared) rather than http://localhost."],"tags":["webhook","ssrf","tls","validation"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}