{"record":{"id":"d8454a4b017ba5aa","repo":"koala73/worldmonitor","slug":"webhook-url-is-not-a-valid-url","errorCode":null,"errorMessage":"Webhook URL is not a valid URL","messagePattern":"Webhook URL is not a valid URL","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":"Thrown by blockedNotificationWebhookUrlReason when `new URL(rawUrl)` raises — i.e. the supplied webhook string is not a parseable absolute URL (missing protocol, unencoded spaces, stray characters). It is the first static gate inside assertNotificationWebhookRegistrationUrlSafe, so it fails fast at webhook registration before any DNS work.","triggerScenarios":"A POST to the notification-webhook registration endpoint with a body whose url field is a bare hostname (e.g. \"example.com/hook\"), a string with spaces, a missing protocol, or any value the URL constructor rejects.","commonSituations":"Frontend form submitting the webhook without forcing an https:// prefix; copy-paste that drops the scheme; a misconfigured integrations panel; a test fixture passing a relative path.","solutions":["Send the URL with an explicit absolute https scheme, e.g. `https://hooks.example.com/webhook`.","Validate client-side before submission: `new URL(value)` in a try/catch and require protocol === 'https:'.","Trim and encode the input string before constructing the URL.","Return a 400 to the caller with a clear message rather than letting the raw error bubble."],"exampleFix":"// before\nregisterWebhook('hooks.example.com/x')\n// after\nregisterWebhook('https://hooks.example.com/x')","handlingStrategy":"validation","validationCode":"function isValidWebhookUrl(rawUrl: string): boolean {\n  try {\n    const u = new URL(rawUrl);\n    return u.protocol === 'https:';\n  } catch {\n    return false;\n  }\n}\n\nif (!isValidWebhookUrl(input)) return res.status(400).json({ error: 'A valid https:// webhook URL is required.' });","typeGuard":"function isWebhookUrl(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 is not a valid URL') {\n    return res.status(400).json({ error: 'Please enter a valid absolute https:// URL.' });\n  }\n  throw err;\n}","preventionTips":["Force the client form to prepend `https://` when the user omits a scheme.","Run `new URL(value)` client-side before submission and disable the submit button until it parses.","Return 400 with a clear message for each static-rejection branch so the user knows which rule fired."],"tags":["webhook","ssrf","validation","url-parsing"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}