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

  1. Only forward absolute http(s) URLs to fetchExternalBrandAsset; reject other schemes at the scraper.
  2. For data: URIs you genuinely want, decode them in-process instead of routing through the network fetcher.
  3. Pre-filter scraped hrefs: if (!/^https?:\/\//i.test(href)) continue;
  4. 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

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


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/f9085963ab76d2f2. Report an issue: GitHub.