{"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":21,"sourceCode":"\n// Recruitee provider — hits the public per-tenant offers API.\n// 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\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":3,"sourceCodeEnd":39,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/recruitee.mjs#L3-L39","documentation":"assertRecruiteeUrl throws when parsed.hostname fails RECRUITEE_HOST_RE (/^[a-z0-9][a-z0-9-]*\\.recruitee\\.com$/). The hostname must be exactly <slug>.recruitee.com — slug starts alphanumeric, then any mix of lowercase alphanumerics and hyphens. This SSRF guard prevents a crafted entry from pointing the fetch at an arbitrary host while still allowing the per-tenant slug to vary.","triggerScenarios":"A hostname like jobs.acme.com (branded, not recruitee.com); acme.RECRUITEE.com (uppercase); acme.recruitee.org (wrong TLD); recruitee.com with no slug; a multi-level subdomain like a.b.recruitee.com (the regex requires exactly one label before .recruitee.com).","commonSituations":"A tenant uses a Recruitee vanity/branded domain that drops the recruitee.com suffix; an entry was misclassified as recruitee when it belongs to a different provider; the slug was typed with an uppercase letter.","solutions":["Use the canonical https://<slug>.recruitee.com subdomain where <slug> is lowercase alphanumeric plus hyphens.","If the board only has a branded domain, the recruitee provider cannot auto-derive it — supply api: pointing to the recruitee.com subdomain or use the correct provider.","Lowercase the hostname and confirm it is a single-label subdomain of recruitee.com.","Check for typos in the TLD (.org, .net vs .com)."],"exampleFix":"// before\n{ careers_url: 'https://jobs.acme.com' }\n// after\n{ careers_url: 'https://acme.recruitee.com' }","handlingStrategy":"validation","validationCode":"const RECRUITEE_HOST_RE = /^[a-z0-9][a-z0-9-]*\\.recruitee\\.com$/;\nfunction isRecruiteeUrl(url) {\n  try {\n    const p = new URL(url);\n    return p.protocol === 'https:' && RECRUITEE_HOST_RE.test(p.hostname);\n  } catch { return false; }\n}\nif (!isRecruiteeUrl(entry.careers_url)) {\n  console.warn(`skip ${entry.name}: not a *.recruitee.com URL`);\n}","typeGuard":"null","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (e) {\n  if (/untrusted hostname/.test(e.message)) {\n    console.warn(`[skip] ${entry.name}: ${e.message}`);\n  } else throw e;\n}","preventionTips":["Use the canonical <slug>.recruitee.com subdomain — branded domains are not auto-derivable.","Lowercase hostnames before validation.","Validate at config-load time against the regex."],"tags":["ssrf","url-validation","hostname-allowlist","provider","recruitee","security"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}