{"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":"Error","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/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/flowxtra.mjs#L17-L53","documentation":"flowxtra.mjs throws this inside assertFlowxtraEndpointUrl(), a host-allowlist guard run before every ctx.fetchJson against the Flowxtra central jobs API. It compares parsed.hostname to the compile-time constant TRUSTED_ENDPOINT_HOST ('app.flowxtra.com') and aborts if they differ, so the request never leaves the process. The guard is an SSRF defense: combined with redirect:'error' it guarantees the only host ever contacted is app.flowxtra.com.","triggerScenarios":"Fires only when the URL passed to assertFlowxtraEndpointUrl() resolves to a hostname other than app.flowxtra.com. In production that URL is built from the module constant JOBS_ENDPOINT ('https://app.flowxtra.com/api/central/jobs'), so a live throw means someone edited JOBS_ENDPOINT/TRUSTED_ENDPOINT_HOST to disagree, a test injected a foreign URL, or the constant was accidentally set to an http/staging host.","commonSituations":"A contributor redirects the provider at a mirror/staging host but changes only JOBS_ENDPOINT and forgets TRUSTED_ENDPOINT_HOST; a unit test calls assertFlowxtraEndpointUrl() directly with a throwaway URL; an env-injection or config-merge accidentally rewrites the constant to a different domain.","solutions":["If you intended to point at app.flowxtra.com, leave JOBS_ENDPOINT and TRUSTED_ENDPOINT_HOST at their shipped values and remove any local override.","If you genuinely need a different host (mirror/proxy), update BOTH JOBS_ENDPOINT and TRUSTED_ENDPOINT_HOST to the same hostname in the same edit.","If this surfaced in a test, pass a URL whose hostname is app.flowxtra.com (e.g. https://app.flowxtra.com/api/central/jobs?page=1) rather than example.com.","Search the repo for any code path that calls assertFlowxtraEndpointUrl() with a non-constant argument and make it derive from JOBS_ENDPOINT instead."],"exampleFix":"// before\nconst JOBS_ENDPOINT = 'https://staging.flowxtra.com/api/central/jobs';\nconst TRUSTED_ENDPOINT_HOST = 'app.flowxtra.com'; // mismatch -> throws\n\n// after\nconst JOBS_ENDPOINT = 'https://app.flowxtra.com/api/central/jobs';\nconst TRUSTED_ENDPOINT_HOST = 'app.flowxtra.com';","handlingStrategy":"validation","validationCode":"// Pre-flight: assert the Flowxtra endpoint constant is intact before scanning.\nfunction checkFlowxtraEndpoint() {\n  const u = new URL('https://app.flowxtra.com/api/central/jobs'); // expected literal\n  if (u.hostname !== 'app.flowxtra.com') throw new Error('flowxtra endpoint host drifted');\n  if (u.protocol !== 'https:') throw new Error('flowxtra endpoint not HTTPS');\n}\n// Run once at startup; abort the scan if it throws.","typeGuard":null,"tryCatchPattern":"// At the scan-runner boundary: isolate one provider's failure so the whole scan continues.\ntry {\n  const jobs = await flowxtraProvider.fetch(entry, ctx);\n} catch (err) {\n  if (/flowxtra: untrusted hostname/.test(err.message)) {\n    console.error(`Skipping ${entry.name}: ${err.message} (constant drift — fix in flowxtra.mjs)`);\n    return [];\n  }\n  throw err;\n}","preventionTips":["Treat JOBS_ENDPOINT and TRUSTED_ENDPOINT_HOST as a paired constant — change them together or not at all.","Never call assertFlowxtraEndpointUrl() with a computed/foreign URL in tests; derive test inputs from JOBS_ENDPOINT.","Add a unit test that asserts new URL(JOBS_ENDPOINT).hostname === TRUSTED_ENDPOINT_HOST so drift fails CI."],"tags":["ssrf-guard","url-validation","host-allowlist","flowxtra","config"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}