{"record":{"id":"771fb968851f7b53","repo":"santifer/career-ops","slug":"jobvite-invalid-url-url","errorCode":null,"errorMessage":"jobvite: invalid URL: ${url}","messagePattern":"jobvite: invalid URL: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/jobvite.mjs","lineNumber":97,"sourceCode":"\n// The XML feed inlines every job's FULL HTML description, so it is large and\n// slow by construction rather than occasionally: Tyler Technologies returns\n// 1.88 MB for 236 jobs in ~11s. That overshoots the shared 10s default in\n// _http.mjs by a second, which aborted the whole tenant and reported it as a\n// 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.","sourceCodeStart":79,"sourceCodeEnd":115,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/jobvite.mjs#L79-L115","documentation":"Thrown by assertJobviteHost when new URL(url) throws — the supplied string is not a parseable absolute URL. In the shipped provider, assertJobviteHost is only ever called on URLs built internally via the URL constructor (buildBoardFetchUrl from an encoded slug, buildFeedUrl via new URL + searchParams.set), which always parse. From the public contract this branch is therefore effectively unreachable; it is an SSRF defense-in-depth guard that would fire only if a constructed URL were somehow malformed (e.g. a slug/eid containing characters that break the constructor — encodeURIComponent and searchParams.set already prevent this).","triggerScenarios":"A fork that passes a raw user/entry string into assertJobviteHost instead of a constructor-built URL; a hypothetical slug/eid containing characters that defeat encodeURIComponent and new URL (not observed in practice); a build mangling the host constants BOARD_HOST/FEED_HOST.","commonSituations":"Extending the provider to accept an entry.api feed URL and routing it through the assert without pre-validation; editing BOARD_HOST/FEED_HOST to a malformed value.","solutions":["Only ever pass URL-constructed strings to assertJobviteHost; pre-validate any caller input with new URL(...) in a try/catch.","Keep BOARD_HOST/FEED_HOST as well-formed hostnames.","Add a unit test asserting assertJobviteHost(buildFeedUrl(eid)) and assertJobviteHost(buildBoardFetchUrl(slug)) do not throw for representative inputs."],"exampleFix":"// before — passing raw entry input\nassertJobviteHost(entry.api);\n\n// after — construct/validate first\nlet u;\ntry { u = new URL(entry.api); } catch { throw new Error('jobvite: bad api url'); }\nassertJobviteHost(u.href);","handlingStrategy":"validation","validationCode":"// The shipped assert only ever receives constructor-built URLs.\n// If you fork it onto caller input, pre-validate first:\nfunction prevalidate(url) {\n  try { return new URL(url).href; }\n  catch { throw new Error(`jobvite: invalid URL: ${url}`); }\n}","typeGuard":"/** True for a parseable absolute URL string. */\nfunction isAbsoluteUrl(value) {\n  if (typeof value !== 'string' || !value.trim()) return false;\n  try { new URL(value); return true; } catch { return false; }\n}","tryCatchPattern":"try {\n  return await jobviteProvider.fetch(entry, ctx);\n} catch (err) {\n  if (/jobvite: invalid URL/.test(err.message)) {\n    // Only reachable if a constructed URL was malformed or caller input bypassed construction.\n    console.error(`jobvite: ${err.message} — pass only URL-constructed strings to assertJobviteHost`);\n  }\n  throw err;\n}","preventionTips":["Only pass URL-constructed strings (new URL(...).href) into assertJobviteHost; never raw entry/user input.","Keep BOARD_HOST/FEED_HOST as well-formed hostnames.","Add a unit test that assertJobviteHost(buildFeedUrl(eid)) and assertJobviteHost(buildBoardFetchUrl(slug)) do not throw for representative inputs."],"tags":["ssrf","url-validation","jobvite","defense-in-depth"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}