{"record":{"id":"c3c69021a2cebbeb","repo":"santifer/career-ops","slug":"lever-untrusted-hostname-parsed-hostname-m","errorCode":null,"errorMessage":"lever: untrusted hostname \"${parsed.hostname}\" — must be one of: ${[...ALLOWED_LEVER_HOSTS].join(', ')}","messagePattern":"lever: untrusted hostname \"(.+?)\" — must be one of: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/lever.mjs","lineNumber":26,"sourceCode":"const ALLOWED_LEVER_HOSTS = new Set(['api.lever.co', 'api.eu.lever.co']);\n\n// The v0 postings endpoint returns the whole board in one response, with every\n// description inlined, so a large board outgrows _http.mjs's 10s default:\n// jobgether is 42.8 MB and aborted at 10s on its own (#4177). Same value and\n// reasoning as ASHBY_TIMEOUT_MS, the other one-response board-wide ATS feed.\nconst LEVER_TIMEOUT_MS = 30_000;\n\n/** @param {string} url */\nfunction assertLeverUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`lever: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`lever: URL must use HTTPS: ${url}`);\n  if (!ALLOWED_LEVER_HOSTS.has(parsed.hostname))\n    throw new Error(`lever: untrusted hostname \"${parsed.hostname}\" — must be one of: ${[...ALLOWED_LEVER_HOSTS].join(', ')}`);\n  return url;\n}\n\n/** @param {import('./_types.js').PortalEntry} entry */\nfunction resolveApiUrl(entry) {\n  // Explicit api: wins — lets an entry keep a human-facing corporate\n  // careers_url (e.g. https://www.coalfire.com/careers) while still pinning\n  // the Lever postings board (mirrors greenhouse's api: precedence).\n  if (entry.api) {\n    assertLeverUrl(entry.api);\n    return entry.api;\n  }\n  let url;\n  try {\n    url = new URL(entry.careers_url || '');\n  } catch {\n    return null;\n  }","sourceCodeStart":8,"sourceCodeEnd":44,"githubUrl":"https://github.com/santifer/career-ops/blob/e7abd431fce9348a95261acac9e0c14779c35df8/providers/lever.mjs#L8-L44","documentation":"assertLeverUrl enforces a host allowlist (ALLOWED_LEVER_HOSTS, e.g. api.lever.co and jobs.lever.co variants). An https URL at any hostname outside the set is rejected; the message lists the actual hostname and the allowed set. This blocks SSRF through attacker-influenced URLs and stops requests to lookalike domains.","triggerScenarios":"Passing an https URL whose parsed hostname is not in ALLOWED_LEVER_HOSTS — a custom job board domain, a corporate proxy, or an entry whose careers_url is a Lever-hosted page at an unexpected subdomain.","commonSituations":"Config entry pointing at a company's own domain that merely embeds Lever jobs, a regional Lever mirror, or swapping api.lever.co for a CDN/mock host during development.","solutions":["Point the URL at a hostname in ALLOWED_LEVER_HOSTS (read the set from providers/lever.mjs).","If the entry is a Lever-powered board, find its underlying api.lever.co or jobs.lever.co endpoint and use that.","If a new legitimate Lever host is needed, add it to ALLOWED_LEVER_HOSTS via a reviewed code change — never bypass the check at runtime.","Use resolveApiUrl's explicit api: override only with an allowed host; the validator still applies."],"exampleFix":"// before\nassertLeverUrl('https://acme.com/api/jobs'); // company's own domain\n// after\nassertLeverUrl('https://api.lever.co/v0/postings/acme?mode=json');","handlingStrategy":"validation","validationCode":"const ALLOWED_LEVER_HOSTS = new Set(['api.lever.co', 'jobs.lever.co']);\nfunction isTrustedLeverHost(u) { try { return ALLOWED_LEVER_HOSTS.has(new URL(u).hostname); } catch { return false; } }\nif (!isTrustedLeverHost(url)) throw new Error(`lever host not allowed: ${url}`);","typeGuard":"function isTrustedLeverUrl(v) { if (typeof v !== 'string') return false; try { return ALLOWED_LEVER_HOSTS.has(new URL(v).hostname) && new URL(v).protocol === 'https:'; } catch { return false; } }","tryCatchPattern":"try {\n  provider.fetch(entry, ctx);\n} catch (e) {\n  if (e.message.includes('untrusted hostname')) {\n    console.error(`Lever entry points at non-Lever host: ${e.message}`);\n    return null;\n  }\n  throw e;\n}","preventionTips":["Resolve Lever-powered boards to their api.lever.co/jobs.lever.co endpoints, not the company's own domain","Keep the host allowlist; extend it only via reviewed code change for new legitimate Lever hosts","Never let user-supplied URLs choose the request host (SSRF defense)","Review config diffs for hostname substitutions"],"tags":["url-validation","ssrf","allowlist","security"],"backgroundTag":"invalid-url","analyzedSha":"e7abd431fce9348a95261acac9e0c14779c35df8","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}