{"record":{"id":"bc59ba21b44b7a4c","repo":"santifer/career-ops","slug":"teamtailor-untrusted-hostname-parsed-hostname","errorCode":null,"errorMessage":"teamtailor: untrusted hostname \"${parsed.hostname}\" — must be <slug>.teamtailor.com (or set \"provider: teamtailor\" to use a branded careers domain)","messagePattern":"teamtailor: untrusted hostname \"(.+?)\" — must be <slug>\\.teamtailor\\.com \\(or set \"provider: teamtailor\" to use a branded careers domain\\)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/teamtailor.mjs","lineNumber":43,"sourceCode":"const TEAMTAILOR_HOST_RE = /^([a-z0-9](?:[a-z0-9-]*[a-z0-9])?)\\.teamtailor\\.com$/i;\n\n/**\n * Validate a feed URL before fetching. Always HTTPS-only. The hostname is\n * pinned to `*.teamtailor.com` for auto-detected entries; an explicit\n * `provider: teamtailor` entry may use its configured branded host.\n * @param {string} url\n * @param {{ explicit?: boolean }} [opts]\n */\nfunction assertFeedUrl(url, { explicit = false } = {}) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`teamtailor: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`teamtailor: URL must use HTTPS: ${url}`);\n  if (!explicit && !TEAMTAILOR_HOST_RE.test(parsed.hostname)) {\n    throw new Error(`teamtailor: untrusted hostname \"${parsed.hostname}\" — must be <slug>.teamtailor.com (or set \"provider: teamtailor\" to use a branded careers domain)`);\n  }\n  return url;\n}\n\n// Derive the RSS feed URL from a tracked_companies entry by normalizing any\n// path on the configured host to /jobs.rss. Auto-detection (explicit=false)\n// only claims *.teamtailor.com hosts; an explicit `provider: teamtailor` entry\n// (explicit=true) may use a branded careers host. Returns null otherwise.\n/**\n * @param {import('./_types.js').PortalEntry} entry\n * @param {{ explicit?: boolean }} [opts]\n */\nfunction resolveFeedUrl(entry, { explicit = false } = {}) {\n  const raw = entry?.api || entry?.careers_url || '';\n  if (typeof raw !== 'string' || !raw) return null;\n  let parsed;\n  try {\n    parsed = new URL(raw);","sourceCodeStart":25,"sourceCodeEnd":61,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/teamtailor.mjs#L25-L61","documentation":"teamtailor only trusts feeds on <slug>.teamtailor.com by default; other hostnames are refused to prevent SSRF. The guard is bypassed by explicitly setting provider: teamtailor on the entry, which signals 'I know this is a branded careers domain'. The error's message says exactly that.","triggerScenarios":"careers_url points at a branded domain like jobs.acme.com while the entry lacks provider: teamtailor; TEAMTAILOR_HOST_RE fails on subdomains like acme.teamtailor.co.uk or a non-teamtailor ATS host misassigned to this provider.","commonSituations":"A company using TeamTailor's custom-domain feature; a moved board (company left TeamTailor, domain now serves another ATS); copy-pasting the public careers homepage URL instead of the feed host.","solutions":["Add provider: teamtailor to the entry to opt into the branded domain","Verify the branded host actually serves the /jobs.rss feed (curl it) before opting in","If the company is no longer on TeamTailor, reassign the correct provider"],"exampleFix":"// before\n- name: Acme\n  careers_url: 'https://jobs.acme.com/jobs.rss'\n// after\n- name: Acme\n  provider: teamtailor\n  careers_url: 'https://jobs.acme.com/jobs.rss'","handlingStrategy":"validation","validationCode":"const u = new URL(entry.careers_url);\nconst isTT = /^[a-z0-9-]+\\.teamtailor\\.com$/i.test(u.hostname);\nif (!isTT && entry.provider !== 'teamtailor') {\n  throw new Error(`${entry.name}: non-teamtailor.com host needs provider: teamtailor to opt in`);\n}","typeGuard":"null","tryCatchPattern":"try {\n  offers = await provider.fetch(entry, ctx);\n} catch (e) {\n  if (e.message.includes('untrusted hostname')) {\n    console.warn(`${entry.name}: branded domain — add provider: teamtailor to the entry`);\n    return [];\n  }\n  throw e;\n}","preventionTips":["Set provider: teamtailor explicitly whenever using a branded careers domain","Verify the branded host serves /jobs.rss before opting in","Re-check hosts after company ATS migrations"],"tags":["ssrf-protection","url-validation","config"],"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"}