{"record":{"id":"f9085963ab76d2f2","repo":"nexu-io/open-design","slug":"unsupported-brand-asset-protocol-parsed-protoco","errorCode":null,"errorMessage":"unsupported brand asset protocol: ${parsed.protocol}","messagePattern":"unsupported brand asset protocol: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/daemon/src/brands/safe-fetch.ts","lineNumber":65,"sourceCode":"}\n\nfunction isIpLiteral(host: string): boolean {\n  return /^\\d{1,3}(?:\\.\\d{1,3}){3}$/.test(host) || host.includes(':');\n}\n\n/**\n * Throw unless `url` is an http(s) URL whose host is a public address — checked\n * both as the literal host and, for a hostname, against every DNS answer.\n */\nexport async function assertPublicBrandUrl(url: string): Promise<void> {\n  let parsed: URL;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`invalid brand asset url: ${String(url)}`);\n  }\n  if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n    throw new Error(`unsupported brand asset protocol: ${parsed.protocol}`);\n  }\n  const host = parsed.hostname.replace(/^\\[/, '').replace(/\\]$/, '').toLowerCase();\n  if (isNonPublicHost(host)) {\n    throw new Error(`blocked non-public brand asset host: ${host}`);\n  }\n  if (!isIpLiteral(host)) {\n    let addresses: Array<{ address: string }>;\n    try {\n      addresses = await dnsPromises.lookup(host, { all: true });\n    } catch {\n      // Let the actual fetch surface a resolution failure rather than masking it.\n      return;\n    }\n    for (const { address } of addresses) {\n      if (isNonPublicHost(String(address))) {\n        throw new Error(\n          `brand asset host resolves to a non-public address: ${host} -> ${address}`,\n        );","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/brands/safe-fetch.ts#L47-L83","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait fetchExternalBrandAsset(logoUrl); // logoUrl = 'data:image/png;base64,...'\n// after\nif (/^data:/i.test(logoUrl)) { /* decode inline */ return; }\nif (!/^https?:\\/\\//i.test(logoUrl)) return;\nawait fetchExternalBrandAsset(logoUrl);","handlingStrategy":"validation","validationCode":"const HTTP_S = /^https?:\\/\\//i;\nfunction isAcceptableAssetUrl(u) {\n  if (!HTTP_S.test(u)) return false;\n  try { new URL(u); return true; } catch { return false; }\n}","typeGuard":"const isHttpUrl = (u: unknown): u is string =>\n  typeof u === 'string' && /^https?:\\/\\//i.test(u);","tryCatchPattern":"try { await fetchExternalBrandAsset(u); }\ncatch (e) {\n  if (String(e.message).startsWith('unsupported brand asset protocol')) continue;\n  throw e;\n}","preventionTips":["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."],"tags":["ssrf","url-validation","brand","protocol","safe-fetch"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}