{"record":{"id":"c4e525f55b05f12c","repo":"santifer/career-ops","slug":"eightfold-untrusted-hostname-parsed-hostname","errorCode":null,"errorMessage":"eightfold: untrusted hostname \"${parsed.hostname}\" — must match *.eightfold.ai","messagePattern":"eightfold: untrusted hostname \"(.+?)\" — must match \\*\\.eightfold\\.ai","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"providers/eightfold.mjs","lineNumber":73,"sourceCode":"\nconst RETRY_POLICY = { retries: 3, baseDelayMs: 500, maxDelayMs: 8_000 };\n\n/**\n * SSRF guard — every request URL passes through here before it is fetched.\n *\n * @param {string} url\n * @returns {string} the same URL, when it is a trusted Eightfold endpoint.\n */\nfunction assertEightfoldUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`eightfold: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`eightfold: URL must use HTTPS: ${url}`);\n  if (!EIGHTFOLD_HOST_RE.test(parsed.hostname)) {\n    throw new Error(`eightfold: untrusted hostname \"${parsed.hostname}\" — must match *.eightfold.ai`);\n  }\n  return url;\n}\n\n/** @param {number} ms @param {any} ctx */\nfunction sleep(ms, ctx) {\n  if (typeof ctx?.sleep === 'function') return ctx.sleep(ms);\n  return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Eightfold reports timestamps as epoch SECONDS (`t_create`, `t_update`), not\n * the ISO strings every other provider gets. Converted here; anything\n * non-finite or non-positive is dropped rather than guessed at.\n *\n * @param {unknown} value\n * @returns {number|undefined} epoch ms, or undefined.\n */","sourceCodeStart":55,"sourceCodeEnd":91,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/eightfold.mjs#L55-L91","documentation":"assertEightfoldUrl is the provider's SSRF guard: every request URL must parse, use HTTPS, and have a hostname matching /^[a-z0-9-]+\\.eightfold\\.ai$/i before it is fetched. This error is thrown when a URL passes parsing and the HTTPS check but its hostname is not an *.eightfold.ai tenant host — e.g. a branded CNAME like careers.<company>.com or a hand-edited host. Eightfold's jobs API is host-pinned to *.eightfold.ai, so non-canonical hosts are refused rather than fetched.","triggerScenarios":"Calling provider.fetch() (which calls assertEightfoldUrl before every page request) with a portal entry whose api/careers_url points at a non-eightfold.ai host — practically this means the built-in apiUrl was tampered with, a custom caller passes its own URL into an exported path, or entry data was mutated between resolveTenant and fetch. resolveTenant itself rejects off-host entries, so the realistic trigger is feeding this guard a URL directly or config drift.","commonSituations":"Configuring a portal entry with the company's branded careers CNAME (talent.bayer.com) instead of the canonical bayer.eightfold.ai; copying a jobs URL that includes a path on a different subdomain; typos like .eightfold.com lookalike hosts; tests asserting the guard fires for hostile hostnames (SSRF probing).","solutions":["Change the portal entry's careers_url (or set entry.api) to the canonical https://<tenant>.eightfold.ai form; branded CNAMEs are deliberately not accepted.","Verify the hostname matches a single-label subdomain of eightfold.ai — no multi-level subdomains (a.b.eightfold.ai fails the regex) and no paths/ports; the API is inferred from the tenant host alone.","If you need to keep a branded careers page for display, keep it in careers_url and pin the tenant host via entry.api, which takes precedence.","If this fires unexpectedly in your own code, log parsed.hostname from the thrown message and diff it against EIGHTFOLD_HOST_RE before retrying."],"exampleFix":"// before (portals.yml entry)\n- name: Bayer\n  careers_url: https://talent.bayer.com/careers\n// after\n- name: Bayer\n  careers_url: https://bayer.eightfold.ai/careers","handlingStrategy":"validation","validationCode":"function isEightfoldUrl(url) {\n  try {\n    const u = new URL(url);\n    return u.protocol === 'https:' && /^[a-z0-9-]+\\.eightfold\\.ai$/i.test(u.hostname);\n  } catch { return false; }\n}\n// before calling: if (!isEightfoldUrl(entry.careers_url)) throw ...","typeGuard":"const isTrustedEightfoldHost = (hostname) => typeof hostname === 'string' && /^[a-z0-9-]+\\.eightfold\\.ai$/i.test(hostname);","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).startsWith('eightfold: untrusted hostname')) {\n    console.error(`Config error: ${entry.name} must use an *.eightfold.ai host, got: ${entry.careers_url}`);\n    return [];\n  }\n  throw err;\n}","preventionTips":["Always point eightfold entries at https://<tenant>.eightfold.ai, never branded CNAMEs","Pin the tenant host via entry.api when careers_url must stay branded","Validate portal entries with the same regex at config load time","Remember the guard rejects multi-level subdomains and non-https schemes"],"tags":["ssrf","url-validation","security","configuration"],"backgroundTag":"invalid-url","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"}