santifer/career-ops · error · Error
ashby: untrusted hostname "${parsed.hostname}" — must be one
Error message
ashby: untrusted hostname "${parsed.hostname}" — must be one of: ${[...ALLOWED_ASHBY_HOSTS].join(', ')} What it means
The host-allowlist step of ashby's SSRF guard. After scheme validation, the hostname must be in `ALLOWED_ASHBY_HOSTS` (currently only `api.ashbyhq.com`). Any other host is rejected so redirects or misconfigured URLs cannot send authenticated-looking requests elsewhere.
Source
Thrown at providers/ashby.mjs:90
min: Math.min(resolvedMin, resolvedMax),
max: Math.max(resolvedMin, resolvedMax),
currency: currency.toUpperCase(),
};
}
const ALLOWED_ASHBY_HOSTS = new Set(['api.ashbyhq.com']);
/** @param {string} url */
function assertAshbyUrl(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
throw new Error(`ashby: invalid URL: ${url}`);
}
if (parsed.protocol !== 'https:') throw new Error(`ashby: URL must use HTTPS: ${url}`);
if (!ALLOWED_ASHBY_HOSTS.has(parsed.hostname))
throw new Error(`ashby: untrusted hostname "${parsed.hostname}" — must be one of: ${[...ALLOWED_ASHBY_HOSTS].join(', ')}`);
return url;
}
/** @param {import('./_types.js').PortalEntry} entry */
function resolveApiUrl(entry) {
// Explicit api: wins — lets an entry keep a human-facing corporate
// careers_url (e.g. https://openai.com/careers) while still pinning the
// Ashby posting-api board (mirrors greenhouse's api: precedence).
if (entry.api) {
assertAshbyUrl(entry.api);
return entry.api;
}
const url = entry.careers_url || '';
const match = url.match(/jobs\.ashbyhq\.com\/([^/?#]+)/);
if (!match) return null;
return `https://api.ashbyhq.com/posting-api/job-board/${match[1]}?includeCompensation=true`;
}
View on GitHub (pinned to 9b17a8ac97)
Solutions
- Pin `api:` to `https://api.ashbyhq.com/posting-api?compId=<tenant>` (the documented posting-API board).
- Keep the human-facing URL in `careers_url` and only use `api:` for the API host — `resolveApiUrl` gives `api:` precedence for exactly this reason.
- If a new legitimate Ashby API host appears, add it to `ALLOWED_ASHBY_HOSTS` explicitly rather than loosening the check.
Example fix
# before (rejected — careers page, not the API host) - name: Acme api: https://careers.acme.com/ # after — keep careers_url for humans, api: for the board - name: Acme careers_url: https://careers.acme.com/ api: https://api.ashbyhq.com/posting-api?compId=acme
Defensive patterns
Strategy: validation
Validate before calling
const ALLOWED = new Set(['api.ashbyhq.com']);
function assertAshbyHost(u) {
const h = new URL(u).hostname;
if (!ALLOWED.has(h)) throw new Error(`ashby: host ${h} not in allowlist`);
}
if (entry.api) assertAshbyHost(entry.api); Prevention
- Always set `api:` to api.ashbyhq.com; keep human URLs in `careers_url`.
- Treat a careers-page URL in `api:` as a config smell.
- Extend the allowlist only with explicit, documented hosts — never with a wildcard.
When it happens
Trigger: `!ALLOWED_ASHBY_HOSTS.has(parsed.hostname)` — the hostname is not exactly `api.ashbyhq.com` (e.g. a corporate careers domain, a staging host, or a look-alike spoof).
Common situations: Setting `api:` to a company's human-facing careers URL instead of the Ashby posting-API host, pointing at a staging mirror, or a typo that changes the host.
Related errors
- arbeitnow: untrusted hostname "${parsed.hostname}" — must be
- bamboohr: untrusted hostname "${parsed.hostname}" — must mat
- breezy: untrusted hostname "${parsed.hostname}" — must match
- larajobs: untrusted hostname "${parsed.hostname}" - must be
- lever: untrusted hostname "${parsed.hostname}" — must be one
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/4e97b8da4814d99a.
Report an issue: GitHub.