Budibase/budibase · error

URL is blocked or could not be resolved safely.

Error message

URL is blocked or could not be resolved safely.

What it means

outboundFetch.ts performs SSRF protection before every request: resolveSafePinnedIp parses the URL, resolves the hostname via the blacklist module's DNS resolver, and requires at least one resolved address. This error means DNS resolution returned no addresses for the hostname, so the library cannot pin a safe IP and refuses to fetch.

Source

Thrown at packages/backend-core/src/utils/outboundFetch.ts:43

    throw new Error("Only HTTP(S) URLs are allowed.")
  }

  if (parsed.username || parsed.password) {
    throw new Error("URL must not include credentials.")
  }

  return parsed
}

function isRedirect(status: number): boolean {
  return [301, 302, 303, 307, 308].includes(status)
}

async function resolveSafePinnedIp(url: string): Promise<string> {
  const parsed = parseUrl(url)
  const addresses = await resolveAddress(parsed.hostname)
  if (addresses.length === 0) {
    throw new Error("URL is blocked or could not be resolved safely.")
  }

  for (const address of addresses) {
    if (await isBlacklisted(address)) {
      throw new Error("URL is blocked or could not be resolved safely.")
    }
  }

  return addresses[0]
}

// Always pin to the first resolved IP address to avoid DNS rebinding attacks.
export function createPinnedLookup(ip: string): LookupFunction {
  const family = ip.includes(":") ? 6 : 4
  return (_hostname, _options, callback) => {
    if (typeof _options === "object" && _options?.all) {
      callback(null, [{ address: ip, family }])
      return

View on GitHub (pinned to a81a902e9a)

Solutions

  1. Verify the hostname with `nslookup <host>` or `dig <host>` from the same environment to confirm DNS actually resolves.
  2. Fix the URL (typos, removed domains, trailing dots/scheme issues) at the source where it is configured.
  3. Fix container/environment DNS configuration (resolv.conf, CoreDNS, VPC DNS) so the resolver can return A/AAAA records.
  4. If the domain is genuinely internal, ensure the process runs where that internal DNS is reachable.

Example fix

// before
await fetchWithBlacklist("https://api.exmaple.com/data")
// after (typo corrected, verified resolvable)
await fetchWithBlacklist("https://api.example.com/data")
Defensive patterns

Strategy: validation

Validate before calling

let parsed: URL
try { parsed = new URL(url) } catch { throw new Error("Invalid URL") }
if (!/^https?:$/.test(parsed.protocol)) throw new Error("Only HTTP(S)")
if (parsed.hostname === "localhost") throw new Error("Blocked host")
// then confirm DNS before fetching:
const addrs = await dns.promises.lookup(parsed.hostname, { all: true })
if (addrs.length === 0) throw new Error("Hostname does not resolve")

Type guard

const isHttpUrl = (url: string): url is string => {
  try {
    const parsed = new URL(url)
    return parsed.protocol === "http:" || parsed.protocol === "https:"
  } catch {
    return false
  }
}

Try / catch

try {
  const res = await fetchWithBlacklist(url)
} catch (err) {
  if (err instanceof Error && err.message.includes("blocked or could not be resolved")) {
    // surface a "hostname unreachable / not allowed" user-facing message
  } else throw err
}

Prevention

When it happens

Trigger: Calling fetchWithBlacklist (via pinnedIp/resolveSafePinnedIp) with a URL whose hostname fails to resolve: empty resolveAddress() result due to a nonexistent domain, broken DNS in the environment, or a hostname that the internal resolver refuses.

Common situations: Typo'd or deleted hostnames in user-supplied webhook/query URLs; containers with no working DNS resolver (/etc/resolv.conf missing or pointing at an unreachable resolver); private hostnames only resolvable via custom DNS the Node process doesn't use.

Related errors


AI-assisted analysis of Budibase/budibase@a81a902e9a (2026-08-29). Data as JSON: /api/errors/258930628fd28e56. Report an issue: GitHub.