{"record":{"id":"d6e59f725f3b4dc8","repo":"payloadcms/payload","slug":"redirect-target-is-not-allowed","errorCode":null,"errorMessage":"Redirect target is not allowed.","messagePattern":"Redirect target is not allowed\\.","errorType":"http","errorClass":"APIError","httpStatus":400,"severity":"warning","filePath":"packages/payload/src/uploads/getExternalFile.ts","lineNumber":85,"sourceCode":"          headers,\n          method: 'GET',\n        })\n      }\n\n      if (res.status >= 300 && res.status < 400) {\n        redirectCount++\n        if (redirectCount > maxRedirects) {\n          throw new APIError(`Too many redirects (max ${maxRedirects})`, 403)\n        }\n        const location = res.headers.get('location')\n        if (location) {\n          fileURL = new URL(location, fileURL).toString()\n          if (\n            uploadConfig.pasteURL &&\n            uploadConfig.pasteURL.allowList &&\n            !isURLAllowed(fileURL, uploadConfig.pasteURL.allowList)\n          ) {\n            throw new APIError('Redirect target is not allowed.', 400)\n          }\n          continue\n        }\n      }\n\n      break\n    }\n\n    if (!res || !res.ok) {\n      throw new APIError(`Failed to fetch file from ${fileURL}`, res?.status)\n    }\n\n    const data = await res.arrayBuffer()\n\n    return {\n      name: filename,\n      data: Buffer.from(data),\n      mimetype: res.headers.get('content-type') || undefined!,","sourceCodeStart":67,"sourceCodeEnd":103,"githubUrl":"https://github.com/payloadcms/payload/blob/00c58b35c0ed348ddc22daabf467b139727214fd/packages/payload/src/uploads/getExternalFile.ts#L67-L103","documentation":"For server-side URL fetching, Payload optionally restricts origins via `upload.pasteURL.allowList`. When following a redirect, the resolved `Location` URL is checked against that allow list; if it does not match, Payload throws `APIError` HTTP 400 `Redirect target is not allowed.` This prevents a redirect from sneaking a file in from an unapproved host (a common SSRF/toxic-proxy bypass).","triggerScenarios":"`upload.pasteURL.allowList` is configured, the fetched URL returns a 3xx, and the `Location` header resolves to a host/path/port/protocol not matching any allow-list entry (entries match `hostname`, optional `pathname`, `port`, `protocol`, `search`).","commonSituations":"An allowed CDN redirects to an internal/secondary host not in the list. The provider moved to a new domain. The allow list was set too narrowly (hostname only, but the redirect lands on a different port/path). `isURLAllowed` matched the initial URL but not the redirect target.","solutions":["Add the redirect target's hostname (and any differing port/protocol) to `upload.pasteURL.allowList`.","Use the direct final URL of the file rather than a URL known to redirect.","Broaden the allow-list entry (e.g. add the CDN's secondary host, or a pathname wildcard) while staying scoped.","Verify the redirect is legitimate and not a malicious open-redirect on the source host."],"exampleFix":"// before\nconst Media = {\n  slug: 'media',\n  upload: {\n    pasteURL: {\n      allowList: [{ hostname: 'cdn.example.com' }], // redirect goes to cdn-secondary.example.com\n    },\n  },\n}\n\n// after\nconst Media = {\n  slug: 'media',\n  upload: {\n    pasteURL: {\n      allowList: [\n        { hostname: 'cdn.example.com' },\n        { hostname: 'cdn-secondary.example.com' },\n      ],\n    },\n  },\n}","handlingStrategy":"validation","validationCode":"import { isURLAllowed } from 'payload/utilities' // or replicate the matcher\n\nfunction targetAllowed(target: string, allowList: AllowList): boolean {\n  return isURLAllowed(target, allowList)\n}\n\nconst allowList = collection.upload?.pasteURL?.allowList ?? []\nif (!targetAllowed(redirectTarget, allowList)) {\n  // add the target host or use a direct URL\n}","typeGuard":"function matchesAllowList(url: string, list: AllowList[]): boolean {\n  try {\n    const u = new URL(url)\n    return list.some((e) => e.hostname === u.hostname &&\n      (!e.port || e.port === u.port) &&\n      (!e.protocol || e.protocol === u.protocol.replace(':', '')))\n  } catch { return false }\n}","tryCatchPattern":"try {\n  await payload.update({ collection: 'media', id, data: { url } })\n} catch (err) {\n  if (err instanceof Error && /redirect target is not allowed/i.test(err.message)) {\n    // broaden upload.pasteURL.allowList with the redirect host, or switch to a direct URL\n  } else throw err\n}","preventionTips":["Include every host a paste-URL provider may redirect to in `allowList`.","Prefer direct, stable URLs over redirecting ones.","Keep allow-list entries scoped (hostname + port + protocol)."],"tags":["security","ssrf","allowlist","redirects","upload"],"backgroundTag":null,"analyzedSha":"00c58b35c0ed348ddc22daabf467b139727214fd","analyzedAt":"2026-08-12T20:45:03.758Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}