{"record":{"id":"ea8fac18abda1006","repo":"santifer/career-ops","slug":"builtin-untrusted-hostname-parsed-hostname-must-be-one-of","errorCode":null,"errorMessage":"builtin: untrusted hostname \"${parsed.hostname}\" — must be one of ${[...new Set(HOSTS.values())].join(', ')}","messagePattern":"builtin: untrusted hostname \"(.+?)\" — must be one of (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"providers/builtin.mjs","lineNumber":174,"sourceCode":" * SSRF guard — every request URL passes through here before it is fetched. The\n * host comes from config, so this is the only thing standing between a\n * portals entry and an arbitrary fetch target. It checks the RESOLVED host\n * against the allowlist again rather than trusting the caller.\n *\n * @param {string} url\n * @returns {string}\n */\nfunction assertHost(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`builtin: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`builtin: URL must use HTTPS: ${url}`);\n  const host = parsed.hostname.toLowerCase();\n  if (HOSTS.get(host) !== host) {\n    throw new Error(`builtin: untrusted hostname \"${parsed.hostname}\" — must be one of ${[...new Set(HOSTS.values())].join(', ')}`);\n  }\n  return url;\n}\n\n/** @param {string} s */\nfunction stripTags(s) {\n  return decodeEntities(String(s).replace(/<[^>]*>/g, ' ')).replace(/\\s+/g, ' ').trim();\n}\n\n/**\n * Text of the first element following an icon marker inside a card.\n * Anchoring on the icon class (rather than on field order) is what keeps this\n * readable when Built In reshuffles the card layout.\n *\n * @param {string} seg  card HTML\n * @param {string} icon FontAwesome class, e.g. 'fa-location-dot'\n * @returns {string} '' when the icon or its text is absent\n */","sourceCodeStart":156,"sourceCodeEnd":192,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/builtin.mjs#L156-L192","documentation":"assertHost in the builtin provider throws this when the URL is valid https but its hostname is not in the static HOSTS allowlist of Built In city domains (e.g. www.builtinseattle.com, www.builtinchicago.com). The host comes from user config, so this allowlist re-check is the only barrier preventing the provider from fetching an arbitrary attacker-chosen URL (SSRF).","triggerScenarios":"A portals.yml entry with host set to an unknown domain ('jobs.acme.com', 'www.builtin.com', a typo like 'builtinseatle.com'), a non-city Built In property, or any unrelated host routed into the builtin provider's fetch path.","commonSituations":"Misspelling a city domain; assuming 'builtin.com' itself is fetchable when only per-city sites are allowlisted; a company careers page that merely links to Built In being configured as if it were a board; adding a new Built In city that this version of the provider does not know yet.","solutions":["Set host to one of the allowlisted values listed verbatim in the error message (e.g. 'www.builtinseattle.com').","Use the exported resolveHost() helper to check a value: it returns null when the host is not allowlisted.","Fix typos in the city domain and drop any scheme/path from the config value.","If a genuinely new Built In city is missing, add it to the HOSTS map in providers/builtin.mjs rather than bypassing the guard."],"exampleFix":"# before\nhost: www.builtin.com\n# after\nhost: www.builtinseattle.com","handlingStrategy":"validation","validationCode":"import { resolveHost } from './providers/builtin.mjs';\nconst canonical = resolveHost(cfg.host);\nif (canonical === null) {\n  throw new Error(`builtin host \"${cfg.host}\" is not an allowlisted Built In city domain`);\n}","typeGuard":"function isAllowlistedBuiltinHost(host) { return typeof host === 'string' && resolveHost(host) !== null; }","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).startsWith('builtin: untrusted hostname')) {\n    console.warn(`Skipping ${entry.name}: host not a Built In city domain`);\n    return null;\n  }\n  throw err;\n}","preventionTips":["Pick hosts from the allowlist echoed in the error message, not from memory.","Call resolveHost() at config-load time to reject unknown hosts before any fetch.","Remember only per-city sites (www.builtin<city>.com) are allowed, not builtin.com itself.","To support a new city, extend the HOSTS map in providers/builtin.mjs — never bypass the guard."],"tags":["url","ssrf","allowlist","security","config"],"backgroundTag":"untrusted-hostname","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}