{"record":{"id":"3aa9baa212c23026","repo":"santifer/career-ops","slug":"smartrecruiters-untrusted-hostname-parsed-host","errorCode":null,"errorMessage":"smartrecruiters: untrusted hostname \"${parsed.hostname}\" — must be one of: ${[...ALLOWED_SMARTRECRUITERS_HOSTS].join(', ')}","messagePattern":"smartrecruiters: untrusted hostname \"(.+?)\" — must be one of: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/smartrecruiters.mjs","lineNumber":74,"sourceCode":"  const parts = [];\n  for (const key of ['companyDescription', 'jobDescription', 'qualifications', 'additionalInformation']) {\n    const text = sections[key]?.text;\n    if (typeof text === 'string' && text.trim()) parts.push(text);\n  }\n  if (parts.length === 0) return '';\n  return htmlToText(parts.join('\\n'));\n}\n\nfunction assertSmartRecruitersUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`smartrecruiters: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`smartrecruiters: URL must use HTTPS: ${url}`);\n  if (!ALLOWED_SMARTRECRUITERS_HOSTS.has(parsed.hostname)) {\n    throw new Error(`smartrecruiters: untrusted hostname \"${parsed.hostname}\" — must be one of: ${[...ALLOWED_SMARTRECRUITERS_HOSTS].join(', ')}`);\n  }\n  return url;\n}\n\nfunction resolveSlug(entry) {\n  // entry.api takes precedence over careers_url (mirrors greenhouse/ashby) so a\n  // branded page (e.g. https://jobs.continental.com) can stay as careers_url\n  // while the SmartRecruiters slug is pinned via\n  // api: https://careers.smartrecruiters.com/<slug> in portals.yml.\n  for (const raw of [entry.api, entry.careers_url]) {\n    if (typeof raw !== 'string' || !raw) continue;\n    let parsed;\n    try {\n      parsed = new URL(raw);\n    } catch {\n      continue;\n    }\n    if (parsed.protocol !== 'https:') continue;","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/smartrecruiters.mjs#L56-L92","documentation":"assertSmartRecruitersUrl pins the hostname to the allowlist ALLOWED_SMARTRECRUITERS_HOSTS, which contains only api.smartrecruiters.com. A syntactically valid HTTPS URL whose hostname is anything else (careers.smartrecruiters.com, a branded custom domain, a lookalike host) is rejected. This is an SSRF-style guard: only provider-constructed API URLs may be fetched.","triggerScenarios":"Calling assertSmartRecruitersUrl with the public careers page (careers.smartrecruiters.com/...) instead of the API host; pointing the provider at a branded domain like jobs.continental.com without an api override; a mistyped host (api.smartrecruiter.com).","commonSituations":"Misunderstanding that careers_url feeds the validator directly — it only supplies the slug; typos in api.smartrecruiters.com; trying to scrape a custom-domain SmartRecruiters tenant by URL instead of configuring provider: smartrecruiters with an api pin.","solutions":["Ensure only URLs on api.smartrecruiters.com are passed to the validator — derive them via buildPostingsUrl(slug)","For a branded careers page, keep careers_url as-is and add api: 'https://careers.smartrecruiters.com/<slug>' so resolveSlug can pin the slug","Fix typos in the hostname and re-run","If a genuinely new official host appears, add it to ALLOWED_SMARTRECRUITERS_HOSTS in providers/smartrecruiters.mjs"],"exampleFix":"// before\nconst url = 'https://careers.smartrecruiters.com/acme/postings'; // careers host, not API\n// after\nconst url = 'https://api.smartrecruiters.com/v1/companies/acme/postings?limit=100&offset=0&status=PUBLIC';","handlingStrategy":"validation","validationCode":"function isSrApiUrl(url) {\n  try {\n    const u = new URL(url);\n    return u.protocol === 'https:' && u.hostname === 'api.smartrecruiters.com';\n  } catch { return false; }\n}\nif (!isSrApiUrl(url)) throw new Error('only api.smartrecruiters.com URLs are fetchable');","typeGuard":"const isSrApiHost = (url) => {\n  try { return new URL(url).hostname === 'api.smartrecruiters.com'; } catch { return false; }\n};","tryCatchPattern":"try {\n  await srProvider.fetch(entry, ctx);\n} catch (e) {\n  if (String(e.message).includes('untrusted hostname')) {\n    console.error(`Entry ${entry.name}: careers pages are not API URLs — pin the slug via api: https://careers.smartrecruiters.com/<slug>`);\n  } else throw e;\n}","preventionTips":["Never pass the public careers URL (careers/jobs.smartrecruiters.com) where an API URL is expected","For branded domains, set entry.api to the careers.smartrecruiters.com slug URL and keep the branded page as careers_url","Build fetch URLs only via buildPostingsUrl(slug)","Check hostnames for typos — the allowlist is exact-match"],"tags":["url-validation","ssrf-guard","hostname-allowlist","config-error"],"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"}