payloadcms/payload · critical · Error

Blocked unsafe attempt to ${hostname}

Error message

Blocked unsafe attempt to ${hostname}

What it means

`safeFetch` is Payload's SSRF defense for outbound fetches (paste-URL, external file fetch, webhooks). It parses the URL's hostname; if the hostname is a literal IP that fails `isSafeIp` (anything whose `ipaddr.js` range is not `unicast` — loopback, private, link-local, multicast, reserved), it throws a plain `Error` `Blocked unsafe attempt to <hostname>` before any network call. This blocks direct-IP SSRF (e.g. `http://169.254.169.254/`, `http://127.0.0.1/`).

Source

Thrown at packages/payload/src/uploads/safeFetch.ts:88

 * - Validates domain names by resolving them to IP addresses and checking if they're safe.
 * - Undici was used because it supported interceptors as well as "credentials: include". Native fetch
 */
export const safeFetch = async (...args: Parameters<typeof undiciFetch>): Promise<Response> => {
  const [unverifiedUrl, options] = args

  try {
    const url = new URL(unverifiedUrl)

    let hostname = url.hostname

    // Strip brackets from IPv6 addresses (e.g., "[::1]" => "::1")
    if (hostname.startsWith('[') && hostname.endsWith(']')) {
      hostname = hostname.slice(1, -1)
    }

    if (ipaddr.isValid(hostname)) {
      if (!isSafeIp(hostname)) {
        throw new Error(`Blocked unsafe attempt to ${hostname}`)
      }
    }
    return (await undiciFetch(url, {
      ...options,
      dispatcher: getSafeDispatcher(),
      redirect: 'manual', // Prevent automatic redirects
    })) as unknown as Response
  } catch (error) {
    if (error instanceof Error) {
      if (error.cause instanceof Error && error.cause.message.includes('unsafe')) {
        // Errors thrown from within interceptors always have 'fetch error' as the message
        // The desired message we want to bubble up is in the cause
        throw new Error(error.cause.message)
      } else {
        let stringifiedUrl: string | undefined = undefined
        if (typeof unverifiedUrl === 'string') {
          stringifiedUrl = unverifiedUrl
        } else if (unverifiedUrl instanceof URL) {

View on GitHub (pinned to 00c58b35c0)

Solutions

  1. Use a public, routable hostname (the SSRF filter allows only `unicast` ranges).
  2. For legitimate internal endpoints, add the URL to `upload.skipSafeFetch` (AllowList) or `pasteURL.allowList` — but only for trusted, intentionally-exposed services.
  3. For local dev, use a hostname that resolves externally, or add a `skipSafeFetch` entry scoped to the dev host.
  4. Never paste cloud metadata IPs (`169.254.169.254`) — the block is intentional and correct.
  5. Audit pasted URLs at the application layer and reject private ranges before they reach `safeFetch`.

Example fix

// before — fetching a private/metadata IP
await payload.update({ collection: 'media', id, data: { url: 'http://169.254.169.254/latest/meta-data/' } })

// after — use a public URL, or explicitly allow a trusted internal host
const Media = {
  slug: 'media',
  upload: {
    pasteURL: { allowList: [{ hostname: 'trusted-internal.corp' }] },
  },
}
Defensive patterns

Strategy: validation

Validate before calling

import ipaddr from 'ipaddr.js'

function isSafeIp(ip: string): boolean {
  try { return ipaddr.parse(ip).range() === 'unicast' } catch { return false }
}

function isLiteralSafeUrl(raw: string): boolean {
  try {
    const u = new URL(raw)
    let h = u.hostname
    if (h.startsWith('[') && h.endsWith(']')) h = h.slice(1, -1)
    return !ipaddr.isValid(h) || isSafeIp(h)
  } catch { return false }
}

if (!isLiteralSafeUrl(pastedUrl)) {
  throw new Error('Refusing URL that points at a non-unicast IP')
}

Type guard

const isLiteralSafeUrl = (raw: string): boolean => {
  try {
    const u = new URL(raw); let h = u.hostname
    if (h.startsWith('[') && h.endsWith(']')) h = h.slice(1, -1)
    return !ipaddr.isValid(h) || ipaddr.parse(h).range() === 'unicast'
  } catch { return false }
}

Try / catch

try {
  await payload.update({ collection: 'media', id, data: { url } })
} catch (err) {
  if (err instanceof Error && /blocked unsafe attempt/i.test(err.message)) {
    // reject the URL or add to skipSafeFetch/allowList only for trusted services
  } else throw err
}

Prevention

When it happens

Trigger: An upload collection fetches an external file (`getExternalFile`) or paste-URL whose URL hostname is a disallowed IP literal — e.g. `http://127.0.0.1`, `http://169.254.169.254` (AWS metadata), `http://10.0.0.1`, `http://[::1]`. `safeFetch` is used unless the URL matches `upload.skipSafeFetch` or `pasteURL.allowList`.

Common situations: A user pastes a URL pointing at internal infrastructure (intentional SSRF probe). A stored document URL was changed to a private IP. Local development with `localhost`/`127.0.0.1` URLs being fetched server-side. IPv6 loopback `[::1]`. A test that uses a literal IP instead of a hostname.

Related errors


AI-assisted analysis of payloadcms/payload@00c58b35c0 (2026-08-12). Data as JSON: /api/errors/17c38b938e59f333. Report an issue: GitHub.