nexu-io/open-design · error · Error
unsupported brand asset protocol: ${parsed.protocol}
Error message
unsupported brand asset protocol: ${parsed.protocol} What it means
Thrown by assertPublicBrandUrl after the URL parsed successfully but its protocol is neither http: nor https:. The SSRF guard only permits http(s) so attacker-controlled schemes (file:, data:, ftp:, gopher:) cannot reach the fetcher.
Source
Thrown at apps/daemon/src/brands/safe-fetch.ts:65
}
function isIpLiteral(host: string): boolean {
return /^\d{1,3}(?:\.\d{1,3}){3}$/.test(host) || host.includes(':');
}
/**
* 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
- Only forward absolute http(s) URLs to fetchExternalBrandAsset; reject other schemes at the scraper.
- For data: URIs you genuinely want, decode them in-process instead of routing through the network fetcher.
- Pre-filter scraped hrefs: if (!/^https?:\/\//i.test(href)) continue;
- Surface scheme errors to the caller as a skipped asset, not a fatal extraction failure.
Example fix
// before
await fetchExternalBrandAsset(logoUrl); // logoUrl = 'data:image/png;base64,...'
// after
if (/^data:/i.test(logoUrl)) { /* decode inline */ return; }
if (!/^https?:\/\//i.test(logoUrl)) return;
await fetchExternalBrandAsset(logoUrl); Defensive patterns
Strategy: validation
Validate before calling
const HTTP_S = /^https?:\/\//i;
function isAcceptableAssetUrl(u) {
if (!HTTP_S.test(u)) return false;
try { new URL(u); return true; } catch { return false; }
} Type guard
const isHttpUrl = (u: unknown): u is string => typeof u === 'string' && /^https?:\/\//i.test(u);
Try / catch
try { await fetchExternalBrandAsset(u); }
catch (e) {
if (String(e.message).startsWith('unsupported brand asset protocol')) continue;
throw e;
} Prevention
- Only http(s) is permitted; reject data:/file:/ftp:/javascript: at the scraper.
- Decode data: URIs inline rather than routing through the network fetcher.
- Surface scheme errors as skipped assets, not fatal extraction failures.
When it happens
Trigger: fetchExternalBrandAsset receives a URL whose scheme is something other than http/https — e.g. 'file:///etc/passwd', 'data:image/png;base64,...', 'ftp://host/logo.png', or 'javascript:alert(1)'. Common in scraped <link>/<img> hrefs that are then handed to the brand fetcher.
Common situations: Inline data: URIs in <img src>; file: URIs from local HTML; a mistyped/copy-pasted URL missing the scheme gets repaired to the wrong scheme; mixed-protocol hrefs from a legacy site.
Related errors
- invalid brand asset url: ${String(url)}
- blocked non-public brand asset host: ${host}
- brand asset host resolves to a non-public address: ${host} -
- too many brand asset redirects (> ${MAX_BRAND_REDIRECTS})
- invalid brand id: ${brandId}
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/f9085963ab76d2f2.
Report an issue: GitHub.