{"record":{"id":"1de9726e94688110","repo":"santifer/career-ops","slug":"bamboohr-untrusted-hostname-parsed-hostname","errorCode":null,"errorMessage":"bamboohr: untrusted hostname \"${parsed.hostname}\" — must match <tenant>.bamboohr.com","messagePattern":"bamboohr: untrusted hostname \"(.+?)\" — must match <tenant>\\.bamboohr\\.com","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"providers/bamboohr.mjs","lineNumber":29,"sourceCode":"//\n// The list endpoint (`/careers/list`) returns lightweight metadata — enough for\n// the Job contract (title, url, location) at zero token cost. The full JD lives\n// behind a second `/careers/<id>/detail` request, which the scanner deliberately\n// skips to stay zero-token (so `description`/`postedAt` are omitted).\n\nconst BAMBOOHR_HOST_RE = /^[a-z0-9][a-z0-9-]*\\.bamboohr\\.com$/;\n\n/** @param {string} url */\nfunction assertBambooHRUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`bamboohr: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`bamboohr: URL must use HTTPS: ${url}`);\n  if (!BAMBOOHR_HOST_RE.test(parsed.hostname)) {\n    throw new Error(`bamboohr: untrusted hostname \"${parsed.hostname}\" — must match <tenant>.bamboohr.com`);\n  }\n  return url;\n}\n\n/**\n * Resolve the tenant origin (`https://<tenant>.bamboohr.com`) from an entry.\n * Honours an explicit `api:` URL, else parses `careers_url`.\n * @param {import('./_types.js').PortalEntry} entry\n * @returns {string | null}\n */\nfunction resolveOrigin(entry) {\n  const rawApi = typeof entry.api === 'string' ? entry.api : '';\n  const rawCareers = typeof entry.careers_url === 'string' ? entry.careers_url : '';\n  const raw = (rawApi || rawCareers).trim();\n  if (!raw) return null;\n  let parsed;\n  try {\n    parsed = new URL(raw);","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/bamboohr.mjs#L11-L47","documentation":"Even a valid HTTPS URL is rejected if its hostname does not match BAMBOOHR_HOST_RE — the pattern enforcing `<tenant>.bamboohr.com`. This confines requests to genuine BambooHR tenant hosts, blocking SSRF via config-supplied URLs pointing at arbitrary machines.","triggerScenarios":"Calling assertBambooHRUrl with hosts like `bamboohr.com` (no tenant), `mycompany.example.com` (custom domain), `mycompany.bamboohr.com.evil.io` (suffix attack), or a tenant containing invalid characters.","commonSituations":"Config entries pointing at a company's corporate domain rather than its `<tenant>.bamboohr.com` board; spoofed/suffixed hostnames; missing tenant subdomain; BambooHR board proxied through the company's own domain.","solutions":["Point the entry at the real tenant host: `https://<tenant>.bamboohr.com` (find the tenant name from the company's BambooHR careers link).","Ensure exactly one subdomain level before `bamboohr.com` — no bare domain, no extra suffix.","If the board is proxied through a corporate domain, use the underlying BambooHR tenant URL instead.","Only if you own the provider: adjust the regex deliberately after confirming the host is a legitimate BambooHR tenant (beware suffix-lookalikes)."],"exampleFix":"// before\nassertBambooHRUrl('https://careers.exampleco.com/api'); // untrusted hostname\n\n// after\nassertBambooHRUrl('https://exampleco.bamboohr.com/careers/list'); // matches <tenant>.bamboohr.com","handlingStrategy":"validation","validationCode":"const BAMBOOHR_HOST_RE = /^[a-z0-9-]+\\.bamboohr\\.com$/;\nfunction isAllowedBambooHost(url) {\n  try { return BAMBOOHR_HOST_RE.test(new URL(url).hostname); } catch { return false; }\n}","typeGuard":"function isBambooReadyEntry(entry) {\n  const origin = resolveOrigin(entry);\n  return origin != null && isAllowedBambooHost(origin);\n}","tryCatchPattern":"try {\n  assertBambooHRUrl(apiUrl);\n} catch (err) {\n  if (/untrusted hostname/.test(err.message)) {\n    console.warn(`Entry hostname is not <tenant>.bamboohr.com: ${err.message}`);\n    return null;\n  }\n  throw err;\n}","preventionTips":["Always use the tenant subdomain form `<tenant>.bamboohr.com` in config.","Reject URLs with extra suffixes after bamboohr.com (spoof/suffix attacks).","Use the real tenant URL rather than a corporate-domain proxy when configuring entries.","Test hostnames against the regex in your config lint, not just at fetch time."],"tags":["url","security","ssrf","allowlist","bamboohr"],"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"}