{"record":{"id":"4e97b8da4814d99a","repo":"santifer/career-ops","slug":"ashby-untrusted-hostname-parsed-hostname-m","errorCode":null,"errorMessage":"ashby: untrusted hostname \"${parsed.hostname}\" — must be one of: ${[...ALLOWED_ASHBY_HOSTS].join(', ')}","messagePattern":"ashby: untrusted hostname \"(.+?)\" — must be one of: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"providers/ashby.mjs","lineNumber":93,"sourceCode":"    min: Math.min(resolvedMin, resolvedMax),\n    max: Math.max(resolvedMin, resolvedMax),\n    currency: currency.toUpperCase(),\n  };\n}\n\nconst ALLOWED_ASHBY_HOSTS = new Set(['api.ashbyhq.com']);\n\n/** @param {string} url */\nfunction assertAshbyUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`ashby: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`ashby: URL must use HTTPS: ${url}`);\n  if (!ALLOWED_ASHBY_HOSTS.has(parsed.hostname))\n    throw new Error(`ashby: untrusted hostname \"${parsed.hostname}\" — must be one of: ${[...ALLOWED_ASHBY_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://openai.com/careers) while still pinning the\n  // Ashby posting-api board (mirrors greenhouse's api: precedence).\n  if (entry.api) {\n    assertAshbyUrl(entry.api);\n    return entry.api;\n  }\n  const url = entry.careers_url || '';\n  const match = url.match(/jobs\\.ashbyhq\\.com\\/([^/?#]+)/);\n  if (!match) return null;\n  return `https://api.ashbyhq.com/posting-api/job-board/${match[1]}?includeCompensation=true`;\n}\n","sourceCodeStart":75,"sourceCodeEnd":111,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/ashby.mjs#L75-L111","documentation":"The final gate in assertAshbyUrl: even an HTTPS URL whose hostname is not in the ALLOWED_ASHBY_HOSTS set is rejected. This is an SSRF/anti-spoofing guard — the provider only ever talks to genuine Ashby board hosts, so a config entry pointing at an arbitrary domain can never be used to make the scanner fetch attacker-controlled endpoints.","triggerScenarios":"Calling assertAshbyUrl with e.g. `https://careers.example.com/api/ashby` or `https://ashby.example.net/...` — hosts that look Ashby-related but are not in ALLOWED_ASHBY_HOSTS (e.g. `jobs.ashby.co`, `api.ashbyhq.com`).","commonSituations":"Config entries pointing at a company's own careers site instead of its Ashby-hosted board, typos like `jobs.ashby.com` (wrong TLD), or custom CNAME domains served by Ashby that the allowlist does not cover.","solutions":["Use the real Ashby host for the board — typically `https://jobs.ashby.co/<org>` or the posting API on `jobs.ashbyhq.com`; check the exact allowed hosts in the error message.","If the entry has a corporate careers URL, move it to the entry's human-facing field and set the explicit `api:` field to the Ashby API URL (resolveApiUrl honors an explicit api).","Fix TLD typos: `ashbyhq.com`/`ashby.co` are the real domains, not `ashby.com`.","Only if you maintain the provider: add the verified host to ALLOWED_ASHBY_HOSTS after confirming it genuinely is Ashby-hosted."],"exampleFix":"// before\nassertAshbyUrl('https://careers.exampleco.com/listing'); // untrusted hostname\n\n// after\nassertAshbyUrl('https://jobs.ashby.co/exampleco'); // allowed host","handlingStrategy":"validation","validationCode":"const ALLOWED = new Set(['jobs.ashby.co', 'jobs.ashbyhq.com', 'api.ashbyhq.com']);\nfunction isAllowedAshbyHost(url) {\n  try { return ALLOWED.has(new URL(url).hostname); } catch { return false; }\n}","typeGuard":"function isAshbyEntry(entry) {\n  return typeof entry?.api === 'string' && isAllowedAshbyHost(entry.api);\n}","tryCatchPattern":"try {\n  assertAshbyUrl(apiUrl);\n} catch (err) {\n  if (/untrusted hostname/.test(err.message)) {\n    console.warn(`Entry points at a non-Ashby host; use the real Ashby board URL. ${err.message}`);\n    return null;\n  }\n  throw err;\n}","preventionTips":["Keep the allowlist of Ashby hosts visible in config docs and check entries against it.","Point corporate careers pages at a display field; reserve `api:` for the genuine Ashby endpoint.","Double-check TLDs: ashby.co and ashbyhq.com, not ashby.com.","Treat hostname allowlist failures as config bugs, not runtime retries."],"tags":["url","security","ssrf","allowlist","ashby"],"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"}