{"record":{"id":"0ee75f7be8953122","repo":"Budibase/budibase","slug":"invalid-url","errorCode":null,"errorMessage":"Invalid URL.","messagePattern":"Invalid URL\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/backend-core/src/utils/outboundFetch.ts","lineNumber":21,"sourceCode":"import { isBlacklisted, resolveAddress } from \"../blacklist\"\nimport fetch, { Headers, RequestInit, Response } from \"node-fetch\"\nimport type { LookupFunction } from \"net\"\n\nconst 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> {","sourceCodeStart":3,"sourceCodeEnd":39,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/backend-core/src/utils/outboundFetch.ts#L3-L39","documentation":"parseUrl() in the SSRF-safe outbound fetch helper wraps `new URL(url)` in try/catch and throws a plain Error('Invalid URL.') when the WHATWG URL parser rejects the input (malformed or non-absolute URL). Every outbound request through this helper validates its target URL first, so any fetch with an unparseable URL fails here.","triggerScenarios":"Calling outboundFetch functions (fetch/get etc.) with a URL like \"example.com/api\" (missing scheme), an empty string, whitespace, or other input that `new URL()` cannot parse.","commonSituations":"User-supplied webhook/query URLs stored without a scheme; template-interpolated URLs producing \"https://undefined/...\"-adjacent garbage; config values missing \"https://\" prefix; trailing spaces from form input.","solutions":["Prepend the scheme: use absolute URLs like \"https://example.com/path\"","Trim and validate the URL string before calling - encode/escape any user-supplied segments","Store normalized URLs at write time (e.g. new URL(input).toString()) so bad values never reach the fetch","Add upstream form validation requiring a full absolute URL"],"exampleFix":"// before\nawait outboundFetch(\"example.com/api\") // Invalid URL.\n// after\nawait outboundFetch(\"https://example.com/api\")","handlingStrategy":"validation","validationCode":"// validate the URL before fetching\nfunction isParsableUrl(u: string): boolean {\n  try {\n    const parsed = new URL(u)\n    return parsed.protocol === \"http:\" || parsed.protocol === \"https:\"\n  } catch {\n    return false\n  }\n}\nif (!isParsableUrl(url)) throw new Error(\"Provide an absolute http(s) URL\")","typeGuard":"function isAbsoluteHttpUrl(u: string): u is string {\n  try {\n    return new URL(u).protocol.startsWith(\"http\")\n  } catch {\n    return false\n  }\n}","tryCatchPattern":"try {\n  const res = await outboundFetch(url)\n} catch (e: any) {\n  if (e?.message === \"Invalid URL.\") {\n    // normalize: trim, prepend https:// if scheme missing, then retry once\n  } else throw e\n}","preventionTips":["Always store absolute URLs with scheme (https://...)","Trim whitespace from user input before persisting URLs","Normalize with new URL(input).toString() at write time","Require full URLs in forms rather than host/path pairs"],"tags":["url","validation","input"],"backgroundTag":"invalid-url","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}