{"record":{"id":"3a8ab29ede5dbcf7","repo":"santifer/career-ops","slug":"flowxtra-untrusted-hostname-parsed-hostname","errorCode":null,"errorMessage":"flowxtra: untrusted hostname \"${parsed.hostname}\" — must be ${TRUSTED_ENDPOINT_HOST}","messagePattern":"flowxtra: untrusted hostname \"(.+?)\" — must be (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"providers/flowxtra.mjs","lineNumber":35,"sourceCode":"\nconst JOBS_ENDPOINT = 'https://app.flowxtra.com/api/central/jobs';\nconst TRUSTED_ENDPOINT_HOST = 'app.flowxtra.com';\nconst TRUSTED_APPLY_HOST = 'flowxtra.com';\nconst PER_PAGE = 100;\nconst DEFAULT_MAX_PAGES = 3;\nconst MAX_PAGES_CAP = 50;\n\n/** @param {string} url */\nfunction assertFlowxtraEndpointUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`flowxtra: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`flowxtra: URL must use HTTPS: ${url}`);\n  if (parsed.hostname !== TRUSTED_ENDPOINT_HOST) {\n    throw new Error(`flowxtra: untrusted hostname \"${parsed.hostname}\" — must be ${TRUSTED_ENDPOINT_HOST}`);\n  }\n  return url;\n}\n\n/** Resolve the page cap: a positive integer `max_pages` on the entry, capped. */\nfunction resolveMaxPages(entry) {\n  const v = entry?.max_pages;\n  if (Number.isInteger(v) && v > 0) return Math.min(v, MAX_PAGES_CAP);\n  return DEFAULT_MAX_PAGES;\n}\n\n// NaN-safe Date.parse — `|| undefined` would also coerce a valid epoch 0.\nfunction toEpochMs(value) {\n  if (!value) return undefined;\n  const parsed = Date.parse(value);\n  return Number.isNaN(parsed) ? undefined : parsed;\n}\n","sourceCodeStart":17,"sourceCodeEnd":53,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/flowxtra.mjs#L17-L53","documentation":"This is the hostname allowlist check inside assertFlowxtraEndpointUrl in providers/flowxtra.mjs. After verifying the URL parses and uses HTTPS, the function requires parsed.hostname to exactly equal TRUSTED_ENDPOINT_HOST; any other host is rejected. This pins the Flowxtra integration to a single known endpoint so a misconfigured or attacker-supplied URL cannot redirect API traffic elsewhere.","triggerScenarios":"Calling the provider with an entry whose endpoint URL parses as HTTPS but whose hostname differs from TRUSTED_ENDPOINT_HOST — e.g. a mirror domain, a typo'd subdomain (flowxtra2.example.com), a lookalike domain, or a custom endpoint injected via config.","commonSituations":"Copy-pasting a staging or vanity URL from a browser; a company migrating ATS vendors leaves a stale custom host in portals.yml; typo-squat domains in scraped or third-party config; attempting to point the provider at a self-hosted proxy.","solutions":["Set the endpoint host back to the trusted host the provider expects (the TRUSTED_ENDPOINT_HOST constant value).","Verify the hostname is not a typo (subdomain spelling, TLD).","If you truly need a different endpoint, update the TRUSTED_ENDPOINT_HOST constant in the provider as a deliberate code change, not via config.","Route through a proxy that terminates on the trusted host if network restrictions are the reason for the alternate URL."],"exampleFix":"// before\nendpoint: \"https://jobs.flowxtra-cdn.example.com/api/jobs\"\n// after\nendpoint: \"https://flowxtra.example.com/api/jobs\" // exact TRUSTED_ENDPOINT_HOST","handlingStrategy":"validation","validationCode":"const TRUSTED = 'flowxtra.example.com'; // keep in sync with the provider\nfunction isTrustedEndpoint(u) {\n  try { return new URL(u).hostname === TRUSTED; } catch { return false; }\n}","typeGuard":"const isTrustedHost = (u, trusted) => { try { return new URL(u).hostname === trusted; } catch { return false; } };","tryCatchPattern":"try {\n  await provider.fetch(entry);\n} catch (e) {\n  if (e.message.includes('untrusted hostname')) {\n    console.error(`Entry \"${entry.name}\" points at a non-allowlisted host — restore ${TRUSTED}`);\n    return;\n  }\n  throw e;\n}","preventionTips":["Copy endpoint URLs only from the provider's official documentation, not from browser address bars of mirrors.","Keep a single source of truth for the endpoint in config instead of per-entry overrides.","Diff portals.yml changes for URL edits during review."],"tags":["security","allowlist","url-validation","config"],"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"}