{"record":{"id":"94a4625f7a9fa49d","repo":"santifer/career-ops","slug":"recruitee-untrusted-hostname-parsed-hostname","errorCode":null,"errorMessage":"recruitee: untrusted hostname \"${parsed.hostname}\" — must match <slug>.recruitee.com","messagePattern":"recruitee: untrusted hostname \"(.+?)\" — must match <slug>\\.recruitee\\.com","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/recruitee.mjs","lineNumber":23,"sourceCode":"// Auto-detects from careers_url pattern `https://<slug>.recruitee.com`.\n// Per-tenant subdomains are the variable part — SSRF defence uses a\n// regex match on `<safe-slug>.recruitee.com` rather than a static\n// allowlist.\n\nimport { htmlToText } from './_html-to-text.mjs';\n\nconst RECRUITEE_HOST_RE = /^[a-z0-9][a-z0-9-]*\\.recruitee\\.com$/;\n\nfunction assertRecruiteeUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`recruitee: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`recruitee: URL must use HTTPS: ${url}`);\n  if (!RECRUITEE_HOST_RE.test(parsed.hostname)) {\n    throw new Error(`recruitee: untrusted hostname \"${parsed.hostname}\" — must match <slug>.recruitee.com`);\n  }\n  return url;\n}\n\nfunction resolveApiUrl(entry) {\n  const raw = typeof entry.careers_url === 'string' ? entry.careers_url : '';\n  if (!raw) return null;\n  let parsed;\n  try {\n    parsed = new URL(raw);\n  } catch {\n    return null;\n  }\n  if (parsed.protocol !== 'https:') return null;\n  if (!RECRUITEE_HOST_RE.test(parsed.hostname)) return null;\n  return `https://${parsed.hostname}/api/offers/`;\n}\n","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/recruitee.mjs#L5-L41","documentation":"The Recruitee API may only be called on <slug>.recruitee.com hosts; this is the provider's SSRF defence, since the fetched URL is server-side requested. Any hostname not matching the /^\\[a-z0-9\\]\\[a-z0-9-\\]*\\.recruitee\\.com$/ regex (custom domains, lookalike hosts, bare domains) is rejected.","triggerScenarios":"A careers_url pointing at a custom careers domain (careers.example.com), a different tenant host, a subdomain of recruitee.com deeper than one label, or uppercase/invalid slug characters reaches assertRecruiteeUrl.","commonSituations":"A company serves its Recruitee board behind its own domain (very common — e.g. careers.hostaway.com) while the config stores only that custom URL; someone configures a proxy or mirror host; a typo like acme.recruitee.com.evil.io.","solutions":["Resolve the custom domain to its underlying <slug>.recruitee.com host and configure that as careers_url","Check the DNS/CNAME of the custom domain — it usually aliases <slug>.recruitee.com","Ensure the slug uses only lowercase letters, digits and hyphens and is a single label under recruitee.com","If the board is genuinely not on recruitee.com, this provider does not apply — use the correct provider"],"exampleFix":"// before\ncareers_url: https://careers.hostaway.com\n// after\ncareers_url: https://hostaway.recruitee.com","handlingStrategy":"validation","validationCode":"const RE = /^[a-z0-9][a-z0-9-]*\\.recruitee\\.com$/;\nfunction isRecruiteeHost(s) {\n  try { return RE.test(new URL(s).hostname); } catch { return false; }\n}\nif (!isRecruiteeHost(entry.careers_url)) throw new Error(`Resolve custom domain to <slug>.recruitee.com for ${entry.name}`);","typeGuard":"function isRecruiteeTenantUrl(v) {\n  try {\n    const u = new URL(String(v));\n    return u.protocol === 'https:' && /^[a-z0-9][a-z0-9-]*\\.recruitee\\.com$/.test(u.hostname);\n  } catch { return false; }\n}","tryCatchPattern":"try {\n  assertRecruiteeUrl(entry.careers_url);\n} catch (err) {\n  const m = err.message.match(/untrusted hostname \"([^\"]+)\"/);\n  if (m) console.error(`Host ${m[1]} is not *.recruitee.com — resolve its CNAME to find the tenant slug`);\n  throw err;\n}","preventionTips":["Resolve custom careers domains (dig CNAME) to the underlying <slug>.recruitee.com and configure that","Never configure proxies, mirrors, or non-recruitee.com hosts for this provider","Remember the guard is an SSRF defence — only single-label slugs under recruitee.com pass"],"tags":["security","ssrf","url-validation"],"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"}