santifer/career-ops · error · Error
smartrecruiters: untrusted hostname "${parsed.hostname}" — m
Error message
smartrecruiters: untrusted hostname "${parsed.hostname}" — must be one of: ${[...ALLOWED_SMARTRECRUITERS_HOSTS].join(', ')} What it means
assertSmartRecruitersUrl is an SSRF defense-in-depth guard: before any fetch it parses the URL built by buildPostingsUrl and rejects any whose hostname is not in the allowlist ALLOWED_SMARTRECRUITERS_HOSTS (currently just 'api.smartrecruiters.com'). In normal operation the URL is constructed internally from a slug, so this check almost never fires against config input — it exists to catch a regression in buildPostingsUrl or an off-host redirect target.
Source
Thrown at providers/smartrecruiters.mjs:24
// `https://(careers|jobs).smartrecruiters.com/<slug>`. A tracked_companies
// entry can also set `provider: smartrecruiters` explicitly to bypass
// detection (useful when the public careers URL is a branded custom domain).
const ALLOWED_SMARTRECRUITERS_HOSTS = new Set(['api.smartrecruiters.com']);
const SR_CAREERS_HOSTS = new Set(['careers.smartrecruiters.com', 'jobs.smartrecruiters.com']);
const SR_PAGE_SIZE = 100;
const SR_MAX_PAGES = 50; // safety cap (5000 postings @ 100/page)
function assertSmartRecruitersUrl(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
throw new Error(`smartrecruiters: invalid URL: ${url}`);
}
if (parsed.protocol !== 'https:') throw new Error(`smartrecruiters: URL must use HTTPS: ${url}`);
if (!ALLOWED_SMARTRECRUITERS_HOSTS.has(parsed.hostname)) {
throw new Error(`smartrecruiters: untrusted hostname "${parsed.hostname}" — must be one of: ${[...ALLOWED_SMARTRECRUITERS_HOSTS].join(', ')}`);
}
return url;
}
function resolveSlug(entry) {
// entry.api takes precedence over careers_url (mirrors greenhouse/ashby) so a
// branded page (e.g. https://jobs.continental.com) can stay as careers_url
// while the SmartRecruiters slug is pinned via
// api: https://careers.smartrecruiters.com/<slug> in portals.yml.
for (const raw of [entry.api, entry.careers_url]) {
if (typeof raw !== 'string' || !raw) continue;
let parsed;
try {
parsed = new URL(raw);
} catch {
continue;
}
if (parsed.protocol !== 'https:') continue;View on GitHub (pinned to 9b17a8ac97)
Solutions
- If you forked buildPostingsUrl to target a new SmartRecruiters API host, add that exact host to ALLOWED_SMARTRECRUITERS_HOSTS at providers/smartrecruiters.mjs:10
- Restore buildPostingsUrl to the shipped template: https://api.smartrecruiters.com/v1/companies/<slug>/postings?limit=...&offset=...&status=PUBLIC
- If you meant to scan a company on a different ATS, switch the entry to the matching provider (greenhouse/ashby/lever/workday) instead of smartrecruiters
- Do not call assertSmartRecruitersUrl with external URLs — it validates only internally-built URLs
Defensive patterns
Strategy: validation
Validate before calling
// assertSmartRecruitersUrl validates an internally-built URL; before calling it,
// confirm buildPostingsUrl produced an allowlisted host so the guard never fires.
const built = buildPostingsUrl(slug, offset);
const u = new URL(built);
if (u.hostname !== 'api.smartrecruiters.com') {
throw new Error(`buildPostingsUrl regression: unexpected host ${u.hostname}`);
} Prevention
- Treat ALLOWED_SMARTRECRUITERS_HOSTS and buildPostingsUrl as a paired contract — edit one, update the other.
- Never route user/config URLs into assertSmartRecruitersUrl; only internally-built URLs belong there.
- Add a unit test asserting buildPostingsUrl output always passes assertSmartRecruitersUrl.
When it happens
Trigger: buildPostingsUrl(slug, offset) emitted a URL whose hostname is not 'api.smartrecruiters.com', e.g. the template constant was edited, ALLOWED_SMARTRECRUITERS_HOSTS was emptied, or assertSmartRecruitersUrl was called directly with a hand-supplied URL. Not reachable from a portals.yml entry on its own.
Common situations: A fork changes the postings endpoint host without updating the allowlist; a test stubs buildPostingsUrl to return a different origin; someone passes a user/config URL straight into assertSmartRecruitersUrl instead of going through resolveSlug/buildPostingsUrl.
Related errors
- themuse: invalid URL: ${url}
- themuse: URL must use HTTPS: ${url}
- themuse: untrusted hostname "${parsed.hostname}" — must be $
- flowxtra: untrusted hostname "${parsed.hostname}" — must be
- gem: invalid URL: ${url}
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/3aa9baa212c23026.
Report an issue: GitHub.