nexu-io/open-design · warning · Error
too many brand asset redirects (> ${MAX_BRAND_REDIRECTS})
Error message
too many brand asset redirects (> ${MAX_BRAND_REDIRECTS}) What it means
fetchExternalBrandAsset follows redirects manually (redirect: 'manual') so every hop can be re-validated by assertPublicBrandUrl. After MAX_BRAND_REDIRECTS (5) hops, another 3xx with a Location throws rather than following further. This bounds redirect chains so an attacker cannot loop or exhaust the daemon.
Source
Thrown at apps/daemon/src/brands/safe-fetch.ts:167
let target = url;
for (let hop = 0; ; hop += 1) {
// Re-validate every hop's host (initial URL and each redirect target) before
// the request, so a public site can't 3xx us into private space. The real
// SSRF stop is the pinned dispatcher below; this is a cheap URL-level
// pre-check (protocol, literal private IPs, redirect target).
await assertPublicBrandUrl(target);
const res = await fetch(target, {
...init,
redirect: 'manual',
dispatcher: brandAssetDispatcher as unknown as NonNullable<RequestInit['dispatcher']>,
});
const location =
res.status >= 300 && res.status < 400 ? res.headers.get('location') : null;
if (!location) return res;
// Drain the redirect response body before following it or bailing out.
if (res.body) await res.body.cancel().catch(() => {});
if (hop >= MAX_BRAND_REDIRECTS) {
throw new Error(`too many brand asset redirects (> ${MAX_BRAND_REDIRECTS})`);
}
// Resolve a possibly-relative Location; the next loop re-validates it and the
// same pinned dispatcher re-binds the new connection.
target = new URL(location, target).toString();
}
}
View on GitHub (pinned to 5be4028344)
Solutions
- Resolve the final asset URL out-of-band (curl -I) and pass the terminal URL directly so no redirect chain is walked.
- If the chain is legitimate and short, confirm none of the intermediate hosts are non-public; then raise MAX_BRAND_REDIRECTS in a focused change with security review.
- Skip this asset and let the extraction continue with a fallback (logo/seed/imagery fallbacks all tolerate a missing asset).
- Investigate redirect loops — they are often a symptom of an auth wall or a broken CDN config, not a real asset.
Example fix
// before const res = await fetchExternalBrandAsset(shortUrl); // >5 hops // after — pre-resolve and fetch the terminal URL const terminal = await resolveFinalUrl(shortUrl); // your own helper, also SSRF-checked const res = await fetchExternalBrandAsset(terminal);
Defensive patterns
Strategy: fallback
Validate before calling
const MAX = 5; // keep in sync with safe-fetch MAX_BRAND_REDIRECTS
async function headResolve(url) {
let target = url, hops = 0;
while (hops++ <= MAX) {
const res = await fetch(target, { method: 'HEAD', redirect: 'manual' });
const loc = res.headers.get('location');
if (!loc || res.status < 300 || res.status >= 400) return target;
target = new URL(loc, target).toString();
}
return null;
} Try / catch
try { return await fetchExternalBrandAsset(u); }
catch (e) {
if (String(e.message).startsWith('too many brand asset redirects')) {
return null; // fall back to a default asset
}
throw e;
} Prevention
- Resolve the terminal URL out-of-band and pass it directly.
- Tolerate a missing asset via fallbacks rather than following long chains.
- Investigate redirect loops — they often indicate an auth wall.
When it happens
Trigger: A brand asset URL whose chain of 3xx responses exceeds five hops — e.g. a CDN edge -> CDN origin -> auth gate -> login redirect -> another redirect -> another, all with Location headers. Also triggered by an accidental redirect loop that re-issues the same Location.
Common situations: Assets behind a sequence of shortener -> CDN -> auth -> final; a misconfigured server returning a redirect cycle; the original asset moved several times; an authenticated CDN that keeps bouncing to a login page.
Related errors
- invalid brand asset url: ${String(url)}
- blocked non-public brand asset host: ${host}
- unsupported brand asset protocol: ${parsed.protocol}
- brand asset host resolves to a non-public address: ${host} -
- Could not fetch ${url} — the site may block server-side requ
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/5f4c1976fc3bae0b.
Report an issue: GitHub.