nexu-io/open-design · error · Error
blocked non-public brand asset host: ${host}
Error message
blocked non-public brand asset host: ${host} What it means
Thrown when the URL's literal hostname (before any DNS lookup) is itself a non-public address: loopback (127.0.0.0/8, ::1, localhost), RFC1918 (10/8, 172.16/12, 192.168/16), CGNAT (100.64/10), link-local/metadata (169.254.169.254), IPv4 multicast (>=224), IPv6 multicast (ff00::/8), ULA (fc00::/7), or unspecified. This is the first SSRF stop, evaluated against the host string as written.
Source
Thrown at apps/daemon/src/brands/safe-fetch.ts:69
}
/**
* Throw unless `url` is an http(s) URL whose host is a public address — checked
* both as the literal host and, for a hostname, against every DNS answer.
*/
export async function assertPublicBrandUrl(url: string): Promise<void> {
let parsed: URL;
try {
parsed = new URL(url);
} catch {
throw new Error(`invalid brand asset url: ${String(url)}`);
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
throw new Error(`unsupported brand asset protocol: ${parsed.protocol}`);
}
const host = parsed.hostname.replace(/^\[/, '').replace(/\]$/, '').toLowerCase();
if (isNonPublicHost(host)) {
throw new Error(`blocked non-public brand asset host: ${host}`);
}
if (!isIpLiteral(host)) {
let addresses: Array<{ address: string }>;
try {
addresses = await dnsPromises.lookup(host, { all: true });
} catch {
// Let the actual fetch surface a resolution failure rather than masking it.
return;
}
for (const { address } of addresses) {
if (isNonPublicHost(String(address))) {
throw new Error(
`brand asset host resolves to a non-public address: ${host} -> ${address}`,
);
}
}
}
}View on GitHub (pinned to 5be4028344)
Solutions
- Treat this throw as correct behavior — do not weaken isNonPublicHost. Investigate where the private URL entered the pipeline.
- If the URL came from a scraped page, the source site is hostile or compromised; quarantine that brand extraction.
- For dev/test, run the local mock on a hostname that resolves to a public IP, or disable network fallbacks via a feature flag rather than pointing at loopback.
- Audit the extraction input (sourceUrl + discovered hrefs) and reject the brand if a private literal slips through upstream filtering.
Example fix
// before — local mock hard-coded in fixture
const logoUrl = 'http://127.0.0.1:9000/logo.svg';
await fetchExternalBrandAsset(logoUrl);
// after — gate network fallbacks in dev, never route private IPs through safe-fetch
if (process.env.NODE_ENV === 'test') { /* use local fixture file, not fetch */ return; }
await fetchExternalBrandAsset(publicLogoUrl); Defensive patterns
Strategy: try-catch
Validate before calling
import { isLoopbackApiHost, isBlockedExternalApiHostname } from '@open-design/contracts/api/connectionTest';
function looksPublicLiteral(host) {
const h = host.toLowerCase();
if (/^ff[0-9a-f]{2}:/.test(h)) return false;
return !isLoopbackApiHost(h) && !isBlockedExternalApiHostname(h);
} Try / catch
try { await fetchExternalBrandAsset(u); }
catch (e) {
if (String(e.message).startsWith('blocked non-public brand asset host')) {
// security stop — quarantine the source, do not retry
recordSuspiciousAsset(u);
return null;
}
throw e;
} Prevention
- Treat the throw as a security stop; never weaken isNonPublicHost.
- Never point brand fixtures at loopback or RFC1918 addresses.
- Audit scraped hrefs and quarantine brands that yield private literals.
When it happens
Trigger: An extracted brand asset URL points at 'http://localhost/...', 'http://127.0.0.1:8080', 'http://169.254.169.254/latest/meta-data/', 'http://10.0.0.1/admin', or 'http://[::1]/'. Either an attacker planted it in a scraped page or a test fixture points at a local service.
Common situations: Cloud-metadata exfiltration attempts via scraped hrefs; dev fixtures that point at a local mock server; brand.json committed with a localhost logo URL; an internal-tooling hostname that resolves to private space is mistakenly promoted to production brand data.
Related errors
- invalid brand asset url: ${String(url)}
- brand asset host resolves to a non-public address: ${host} -
- too many brand asset redirects (> ${MAX_BRAND_REDIRECTS})
- unsupported brand asset protocol: ${parsed.protocol}
- 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/3d733eb3628277ab.
Report an issue: GitHub.