{"record":{"id":"fa1c403b70ea5cae","repo":"Budibase/budibase","slug":"only-http-s-urls-are-allowed","errorCode":null,"errorMessage":"Only HTTP(S) URLs are allowed.","messagePattern":"Only HTTP\\(S\\) URLs are allowed\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/backend-core/src/utils/outboundFetch.ts","lineNumber":25,"sourceCode":"const MAX_REDIRECTS = 5\nconst ALLOWED_PROTOCOLS = new Set([\"http:\", \"https:\"])\nconst SENSITIVE_REDIRECT_HEADERS = [\n  \"authorization\",\n  \"cookie\",\n  \"cookie2\",\n  \"proxy-authorization\",\n]\n\nfunction parseUrl(url: string): URL {\n  let parsed: URL\n  try {\n    parsed = new URL(url)\n  } catch {\n    throw new Error(\"Invalid URL.\")\n  }\n\n  if (!ALLOWED_PROTOCOLS.has(parsed.protocol)) {\n    throw new Error(\"Only HTTP(S) URLs are allowed.\")\n  }\n\n  if (parsed.username || parsed.password) {\n    throw new Error(\"URL must not include credentials.\")\n  }\n\n  return parsed\n}\n\nfunction isRedirect(status: number): boolean {\n  return [301, 302, 303, 307, 308].includes(status)\n}\n\nasync function resolveSafePinnedIp(url: string): Promise<string> {\n  const parsed = parseUrl(url)\n  const addresses = await resolveAddress(parsed.hostname)\n  if (addresses.length === 0) {\n    throw new Error(\"URL is blocked or could not be resolved safely.\")","sourceCodeStart":7,"sourceCodeEnd":43,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/backend-core/src/utils/outboundFetch.ts#L7-L43","documentation":"parseUrl() enforces an SSRF-safe protocol allowlist (ALLOWED_PROTOCOLS = http: and https:). If the parsed URL's protocol is anything else - file:, ftp:, data:, javascript:, etc. - it throws Error('Only HTTP(S) URLs are allowed.'). This prevents exfiltration via local file or non-HTTP schemes from server-side outbound requests.","triggerScenarios":"outboundFetch called with \"file:///etc/passwd\", \"ftp://host/file\", \"data:text/plain,...\" or any custom-scheme URL; also triggered by redirects only in the sense that redirects are re-validated elsewhere.","commonSituations":"Pasting browser-style URLs like \"localhost:3000\" (parsed protocol becomes \"localhost:\"); integrations configured with ftp/webhook schemes; malicious payloads probing for SSRF via file://.","solutions":["Use an http:// or https:// URL (use http only for local development)","If the target only supports ftp/etc., fetch it through a separate tool - this helper is HTTP-only","For \"localhost:3000\" style input, rewrite to \"http://localhost:3000\"","Sanitize user input to strip or reject non-HTTP schemes before storing"],"exampleFix":"// before\nawait outboundFetch(\"ftp://files.example.com/data.csv\")\n// after\nawait outboundFetch(\"https://files.example.com/data.csv\")","handlingStrategy":"validation","validationCode":"// reject non-HTTP schemes before fetching\nfunction isHttpUrl(u: string): boolean {\n  try {\n    const proto = new URL(u).protocol\n    return proto === \"http:\" || proto === \"https:\"\n  } catch {\n    return false\n  }\n}\nif (!isHttpUrl(url)) throw new Error(\"Only http(s) URLs supported\")","typeGuard":null,"tryCatchPattern":"try {\n  const res = await outboundFetch(url)\n} catch (e: any) {\n  if (e?.message === \"Only HTTP(S) URLs are allowed.\") {\n    // reject the input or rewrite to an http(s) equivalent\n  } else throw e\n}","preventionTips":["Only configure http/https endpoints for webhooks and queries","Treat file://, ftp://, data: inputs as invalid at form-validation time","Rewrite bare host:port strings (\"localhost:3000\") to include an explicit scheme","Treat this error on production payloads as a possible SSRF probe and log it"],"tags":["ssrf","security","url","validation"],"backgroundTag":"unsupported-url-protocol","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}