{"record":{"id":"3fa20e2ae736cc19","repo":"Budibase/budibase","slug":"url-must-not-include-credentials","errorCode":null,"errorMessage":"URL must not include credentials.","messagePattern":"URL must not include credentials\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/backend-core/src/utils/outboundFetch.ts","lineNumber":29,"sourceCode":"  \"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.\")\n  }\n\n  for (const address of addresses) {\n    if (await isBlacklisted(address)) {","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/backend-core/src/utils/outboundFetch.ts#L11-L47","documentation":"parseUrl() rejects URLs containing embedded userinfo (parsed.username or parsed.password, e.g. https://user:pass@host). These credentials would leak into logs, redirect targets and error messages, so the SSRF-safe fetch helper forbids them outright with Error('URL must not include credentials.').","triggerScenarios":"outboundFetch called with \"https://user:pass@api.example.com/...\"; also basic-auth URLs copied from other tools; a datasource/webhook config where someone embedded auth in the URL.","commonSituations":"Migrating configs from curl commands that used -u user:pass with the URL form; legacy APIs documented with credentials-in-URL; secrets accidentally pasted into URL fields in the builder.","solutions":["Move credentials out of the URL: pass them via an Authorization header (e.g. Basic base64(user:pass)) or the helper's headers option","Use API keys/tokens in headers instead of URL userinfo","If a legacy endpoint requires URL auth, proxy it through a service that injects credentials server-side","Rotate any credentials that were embedded in URLs - they may have been logged"],"exampleFix":"// before\nawait outboundFetch(\"https://user:pass@api.example.com/v1/data\")\n// after\nconst auth = Buffer.from(\"user:pass\").toString(\"base64\")\nawait outboundFetch(\"https://api.example.com/v1/data\", {\n  headers: { Authorization: `Basic ${auth}` },\n})","handlingStrategy":"validation","validationCode":"// detect userinfo in a URL before fetching\nfunction urlHasCredentials(u: string): boolean {\n  try {\n    const parsed = new URL(u)\n    return Boolean(parsed.username || parsed.password)\n  } catch {\n    return true\n  }\n}\nif (urlHasCredentials(url)) throw new Error(\"Move credentials to headers\")","typeGuard":"function isCredentialFreeUrl(u: string): boolean {\n  try {\n    const p = new URL(u)\n    return !p.username && !p.password\n  } catch {\n    return false\n  }\n}","tryCatchPattern":"try {\n  const res = await outboundFetch(url)\n} catch (e: any) {\n  if (e?.message === \"URL must not include credentials.\") {\n    // extract creds, strip from URL, send as Authorization header instead\n  } else throw e\n}","preventionTips":["Never embed basic-auth in URLs; always use headers","Rotate any credentials that were pasted into URL fields","Strip userinfo from URLs at config-ingestion time and store credentials separately","Audit existing datasource/webhook configs for '@' in the authority component"],"tags":["security","credentials","url"],"backgroundTag":"credentials-in-url","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}