santifer/career-ops · error · Error
greenhouse: untrusted hostname "${parsed.hostname}" — must b
Error message
greenhouse: untrusted hostname "${parsed.hostname}" — must be one of: ${[...ALLOWED_GREENHOUSE_HOSTS].join(', ')} What it means
greenhouse.mjs throws this in assertGreenhouseUrl() when the URL is valid HTTPS but its hostname is not in ALLOWED_GREENHOUSE_HOSTS ('boards-api.greenhouse.io', 'boards.greenhouse.io', 'job-boards.greenhouse.io', 'job-boards.eu.greenhouse.io'). It is the host-allowlist half of the SSRF guard; with redirect:'error' it pins every Greenhouse request to a known-good Greenhouse host. A live throw means entry.api (or a derived URL) targets a host outside that set.
Source
Thrown at providers/greenhouse.mjs:24
const ALLOWED_GREENHOUSE_HOSTS = new Set([
'boards-api.greenhouse.io',
'boards.greenhouse.io',
'job-boards.greenhouse.io',
'job-boards.eu.greenhouse.io',
]);
/** @param {string} url */
function assertGreenhouseUrl(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
throw new Error(`greenhouse: invalid URL: ${url}`);
}
if (parsed.protocol !== 'https:') throw new Error(`greenhouse: URL must use HTTPS: ${url}`);
if (!ALLOWED_GREENHOUSE_HOSTS.has(parsed.hostname))
throw new Error(`greenhouse: untrusted hostname "${parsed.hostname}" — must be one of: ${[...ALLOWED_GREENHOUSE_HOSTS].join(', ')}`);
return url;
}
/** @param {import('./_types.js').PortalEntry} entry */
function resolveApiUrl(entry) {
if (entry.api) {
assertGreenhouseUrl(entry.api);
return entry.api;
}
const url = entry.careers_url || '';
const match = url.match(/job-boards(?:\.eu)?\.greenhouse\.io\/([^/?#]+)/);
if (match) return `https://boards-api.greenhouse.io/v1/boards/${match[1]}/jobs`;
return null;
}
// NaN-safe Date.parse — `|| undefined` would also coerce a valid epoch 0.
function toEpochMs(value) {
if (!value) return undefined;View on GitHub (pinned to 9b17a8ac97)
Solutions
- Set api to a URL on boards-api.greenhouse.io (e.g. https://boards-api.greenhouse.io/v1/boards/<slug>/jobs) or remove api and provide a careers_url on job-boards.greenhouse.io so the provider derives the correct API URL.
- If a legitimately new Greenhouse host must be supported, add it to ALLOWED_GREENHOUSE_HOSTS with justification.
- Treat an unexpected internal/private hostname here as a possible SSRF/config-injection attempt and verify the source of api.
Example fix
# before - name: Acme provider: greenhouse api: https://ats-proxy.internal/greenhouse # off-allowlist -> throws # after - name: Acme provider: greenhouse api: https://boards-api.greenhouse.io/v1/boards/acme/jobs
Defensive patterns
Strategy: validation
Validate before calling
// Pre-flight: ensure a Greenhouse entry.api host is allowlisted (SSRF defense at config time).
const GH_HOSTS = new Set(['boards-api.greenhouse.io','boards.greenhouse.io','job-boards.greenhouse.io','job-boards.eu.greenhouse.io']);
function greenhouseApiHostAllowed(entry) {
if (!entry.api) return true; // derived URL stays on boards-api.greenhouse.io
let u;
try { u = new URL(entry.api); } catch { return false; }
return GH_HOSTS.has(u.hostname);
} Prevention
- Omit api and provide a careers_url on job-boards.greenhouse.io so the provider derives a safe boards-api URL.
- Treat an off-allowlist api as a possible SSRF/config-injection attempt and audit its origin.
- If a new legitimate Greenhouse host appears, add it to ALLOWED_GREENHOUSE_HOSTS with a justification comment.
When it happens
Trigger: entry.api is set to a non-Greenhouse host (a proxy, a mirror, or an internal host — the SSRF case the guard exists for); a regional Greenhouse host not yet in the allowlist; a test calls assertGreenhouseUrl('https://evil.com/...').
Common situations: Operator points api at a corporate proxy or a different ATS by mistake; a new Greenhouse regional domain shipped but is not allowlisted; a malicious/injected api value targets an internal host.
Related errors
- flowxtra: untrusted hostname "${parsed.hostname}" — must be
- gem: untrusted hostname "${parsed.hostname}" — must be one o
- getonbrd: untrusted hostname "${parsed.hostname}" — must be
- glints: untrusted hostname "${parsed.hostname}" — must be on
- gem: invalid URL: ${url}
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/7816517c210bb1a1.
Report an issue: GitHub.