{"record":{"id":"87810d121cf9d748","repo":"calcom/cal.diy","slug":"webhook-url-is-not-allowed-validation-error","errorCode":null,"errorMessage":"Webhook URL is not allowed: ${validation.error}","messagePattern":"Webhook URL is not allowed: (.+?)","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"apps/api/v2/src/modules/webhooks/utils/validate-webhook-url.ts","lineNumber":7,"sourceCode":"import { BadRequestException } from \"@nestjs/common\";\nimport { validateUrlForSSRFSync } from \"@calcom/platform-libraries\";\n\nexport function validateWebhookUrl(subscriberUrl: string): void {\n  const validation = validateUrlForSSRFSync(subscriberUrl);\n  if (!validation.isValid) {\n    throw new BadRequestException(`Webhook URL is not allowed: ${validation.error}`);\n  }\n}\n\nexport function validateWebhookUrlIfChanged(\n  newSubscriberUrl: string | undefined,\n  existingSubscriberUrl: string | undefined\n): void {\n  if (newSubscriberUrl && newSubscriberUrl !== existingSubscriberUrl) {\n    validateWebhookUrl(newSubscriberUrl);\n  }\n}\n","sourceCodeStart":1,"sourceCodeEnd":19,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/modules/webhooks/utils/validate-webhook-url.ts#L1-L19","documentation":"validateWebhookUrl runs validateUrlForSSRFSync (an SSRF guard from @calcom/platform-libraries) on the subscriberUrl; if the URL is not allowed it throws BadRequestException (HTTP 400) with the validation error. This blocks internal/loopback/private IPs and disallowed schemes to prevent SSRF via webhook callbacks.","triggerScenarios":"Creating or updating any webhook with a subscriberUrl that resolves to a private/loopback/link-local address, uses a disallowed scheme, or otherwise fails the SSRF policy (e.g. http://localhost, http://127.0.0.1, http://10.x.x.x, http://169.254.169.254).","commonSituations":"Local development pointing webhooks at localhost; cloud metadata IP abuse; DNS that resolves internally; missing https; typos in the URL.","solutions":["Use a publicly routable https URL for the subscriberUrl.","For local testing, expose the callback via a tunnel that yields a public hostname.","Ensure the URL scheme is allowed (typically https).","Resolve the host externally and confirm it is not in a private range before submitting."],"exampleFix":"// before\nbody.subscriberUrl = 'http://localhost:3000/hook';\n// after\nbody.subscriberUrl = 'https://my-app.example.com/hook';","handlingStrategy":"validation","validationCode":"import { isPublicHostname } from './net'; // caller utility\nfunction assertSafeWebhookUrl(url: string) {\n  try { const u = new URL(url); if (u.protocol !== 'https:') throw new Error('https required'); }\n  catch (e) { throw new Error(`bad webhook url: ${e.message}`); }\n  if (/localhost|127\\.|10\\.|192\\.168\\.|172\\.(1[6-9]|2\\d|3[01])\\.|169\\.254\\./.test(url)) {\n    throw new Error('SSRF: private/loopback URL not allowed');\n  }\n}\nassertSafeWebhookUrl(body.subscriberUrl);","typeGuard":"const isProbablySafeWebhookUrl = (u: string): boolean => {\n  try { const url = new URL(u); return url.protocol === 'https:' && !/(localhost|127\\.|10\\.|192\\.168\\.|169\\.254\\.)/.test(url.hostname); }\n  catch { return false; }\n};","tryCatchPattern":"try { await api.createWebhook(body); }\ncatch (e) {\n  if (e.status === 400 && /Webhook URL is not allowed/.test(e.message)) {\n    // prompt user for a public https URL, then retry\n  } else throw e;\n}","preventionTips":["Always use https with a public hostname for webhook callbacks.","Use a tunnel (e.g. ngrok with a stable public host) for local testing.","Client-side validate scheme and hostname before submit."],"tags":["webhooks","ssrf","security","validation","bad-request"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}