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
- Use a public, routable hostname (the SSRF filter allows only `unicast` ranges).
- For legitimate internal endpoints, add the URL to `upload.skipSafeFetch` (AllowList) or `pasteURL.allowList` — but only for trusted, intentionally-exposed services.
- For local dev, use a hostname that resolves externally, or add a `skipSafeFetch` entry scoped to the dev host.
- Never paste cloud metadata IPs (`169.254.169.254`) — the block is intentional and correct.
- 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
- Never paste URLs pointing at private/loopback/metadata IPs.
- Validate URL hostnames at the application layer before storage.
- Keep `safeFetch` enabled in production; only bypass via `skipSafeFetch` for trusted internal services.
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
- ${error.cause.message}
- Redirect target is not allowed.
- File type ${mimeTypeFromExtension} (from extension ${typeFro
- Invalid filename.
- The provided URL is not allowed.
AI-assisted analysis of payloadcms/payload@00c58b35c0 (2026-08-12).
Data as JSON: /api/errors/17c38b938e59f333.
Report an issue: GitHub.