{"record":{"id":"701604688e40cf9d","repo":"santifer/career-ops","slug":"jobvite-untrusted-hostname-parsed-hostname","errorCode":null,"errorMessage":"jobvite: untrusted hostname \"${parsed.hostname}\" — must be ${BOARD_HOST} or ${FEED_HOST}","messagePattern":"jobvite: untrusted hostname \"(.+?)\" — must be (.+?) or (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/jobvite.mjs","lineNumber":102,"sourceCode":"// network failure. Sized to absorb a genuinely big tenant on a slow link; the\n// board page (a normal HTML document) keeps the default.\nconst FEED_TIMEOUT_MS = 45_000;\n\n/**\n * Pin a URL to the two known Jobvite hosts over HTTPS.\n * @param {string} url\n */\nfunction assertJobviteHost(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`jobvite: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:')\n    throw new Error(`jobvite: URL must use HTTPS: ${url}`);\n  if (!ALLOWED_HOSTS.has(parsed.hostname))\n    throw new Error(`jobvite: untrusted hostname \"${parsed.hostname}\" — must be ${BOARD_HOST} or ${FEED_HOST}`);\n  return url;\n}\n\n// NaN-safe Date.parse → epoch ms.\n/** @param {string} value */\nfunction toEpochMs(value) {\n  if (!value) return undefined;\n  const parsed = Date.parse(value);\n  return Number.isNaN(parsed) ? undefined : parsed;\n}\n\n/**\n * The vanity slug from a Jobvite careers URL, or null.\n * Only used to build the board URL for eId discovery.\n *\n * @param {import('./_types.js').PortalEntry} entry\n * @returns {string | null}\n */","sourceCodeStart":84,"sourceCodeEnd":120,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/jobvite.mjs#L84-L120","documentation":"Thrown by assertJobviteHost() when a parsed URL is HTTPS but its hostname is not in the ALLOWED_HOSTS set (the Jobvite board host or the XML feed host). The provider hard-pins the two known Jobvite hosts and will not fetch from any other hostname, preventing SSRF via redirect or a tampered entry pointing at an internal or attacker-controlled server.","triggerScenarios":"An api: URL whose host is something other than the expected Jobvite hosts (e.g. a lookalike domain, a staging host, an internal IP); a careers_url whose vanity slug resolution produced a host the allow-list rejects; or an attempt to point the provider at a non-Jobvite ATS that happens to share a URL shape.","commonSituations":"Pointing the jobvite provider at a company that has migrated off Jobvite to a different ATS (the slug now answers a different host); a typo in the careers_url domain; a company that uses a custom CNAME not in the allow-list.","solutions":["Confirm the entry is genuinely a Jobvite tenant — open the careers_url in a browser and check it resolves to a Jobvite board.","If the company migrated off Jobvite, switch the entry's provider to the correct ATS (greenhouse, lever, ashby, etc.) or remove it.","If it is a valid Jobvite tenant with a custom domain, use the company_eid: field with the canonical app.jobvite.com feed host instead of the vanity URL."],"exampleFix":"// before (portals.yml) — wrong provider/host\nacme:\n  provider: jobvite\n  careers_url: https://careers.acme.com   # not a Jobvite host\n\n// after — pin by eId on the canonical Jobvite feed host\nacme:\n  provider: jobvite\n  company_eid: q6NaVfwI","handlingStrategy":"validation","validationCode":"const JOBVITE_HOSTS = new Set(['jobs.jobvite.com', 'app.jobvite.com']);\nfunction isTrustedJobviteHost(url) {\n  try {\n    return JOBVITE_HOSTS.has(new URL(url).hostname);\n  } catch {\n    return false;\n  }\n}","typeGuard":"/** @param {string} url @returns {boolean} */\nfunction isAllowedJobviteUrl(url) {\n  try {\n    const p = new URL(url);\n    return p.protocol === 'https:' && JOBVITE_HOSTS.has(p.hostname);\n  } catch {\n    return false;\n  }\n}","tryCatchPattern":null,"preventionTips":["Keep a config-time allow-list of expected provider hosts and lint entries against it.","When a company changes ATS, update the provider field, not just the URL.","Prefer company_eid: over vanity URLs so the host is always the canonical app.jobvite.com."],"tags":["jobvite","ssrf-guard","hostname-allowlist","config"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}