santifer/career-ops · error · Error
gem: untrusted hostname "${parsed.hostname}" — must be one o
Error message
gem: untrusted hostname "${parsed.hostname}" — must be one of: ${[...ALLOWED_GEM_HOSTS].join(', ')} What it means
gem.mjs throws this in assertGemUrl() when the URL is valid HTTPS but its hostname is not in ALLOWED_GEM_HOSTS (currently just 'jobs.gem.com'). It is the host-allowlist half of the SSRF guard; combined with redirect:'error' it pins every Gem request to jobs.gem.com. A live throw means GEM_API_URL's hostname drifted from the allowlist.
Source
Thrown at providers/gem.mjs:115
const body = htmlToText(posting?.descriptionHtml);
const outro = htmlToText(posting?.jobPostSectionHtml?.outroHtml);
const compensation = htmlToText(posting?.compensationHtml);
const text = [intro, body, outro].filter(Boolean).join('\n\n');
return compensation ? [text, `Compensation: ${compensation}`].filter(Boolean).join('\n\n') : text;
}
/** @param {string} url */
function assertGemUrl(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
throw new Error(`gem: invalid URL: ${url}`);
}
if (parsed.protocol !== 'https:') throw new Error(`gem: URL must use HTTPS: ${url}`);
if (!ALLOWED_GEM_HOSTS.has(parsed.hostname))
throw new Error(`gem: untrusted hostname "${parsed.hostname}" — must be one of: ${[...ALLOWED_GEM_HOSTS].join(', ')}`);
return url;
}
/** @param {import('./_types.js').PortalEntry} entry */
function resolveBoardId(entry) {
const raw = typeof entry.careers_url === 'string' ? entry.careers_url : '';
if (!raw) return null;
let parsed;
try {
parsed = new URL(raw);
} catch {
return null;
}
if (parsed.hostname !== 'jobs.gem.com') return null;
const match = parsed.pathname.match(/^\/([^/?#]+)/);
return match ? match[1] : null;
}
View on GitHub (pinned to 9b17a8ac97)
Solutions
- Restore GEM_API_URL to https://jobs.gem.com/api/public/graphql/batch.
- If a legitimately different Gem host must be used, add it to ALLOWED_GEM_HOSTS in the same change and document why.
- For tests asserting the guard, keep the throw as the expected behavior rather than 'fixing' the URL.
Example fix
// before const GEM_API_URL = 'https://jobs-gql.gem.com/api/public/graphql/batch'; const ALLOWED_GEM_HOSTS = new Set(['jobs.gem.com']); // mismatch // after const GEM_API_URL = 'https://jobs.gem.com/api/public/graphql/batch'; const ALLOWED_GEM_HOSTS = new Set(['jobs.gem.com']);
Defensive patterns
Strategy: validation
Validate before calling
// Startup invariant: the Gem endpoint host must be in the allowlist.
function checkGemHost() {
const u = new URL('https://jobs.gem.com/api/public/graphql/batch');
if (!ALLOWED_GEM_HOSTS.has(u.hostname)) throw new Error('gem host off allowlist');
} Prevention
- Keep ALLOWED_GEM_HOSTS and GEM_API_URL in the same edit when changing hosts.
- Add a unit test: assert(ALLOWED_GEM_HOSTS.has(new URL(GEM_API_URL).hostname)).
- Treat an unexpected throw here as a possible malicious constant override and audit the source.
When it happens
Trigger: GEM_API_URL was changed to a mirror/CNAME host (e.g. 'https://gem-mirror.example.com/...') without adding it to ALLOWED_GEM_HOSTS; a test calls assertGemUrl('https://evil.com/...'); the allowlist set and the constant were updated out of sync.
Common situations: Contributor adds a regional Gem host but edits only the constant; a security test harness throws foreign hostnames at the guard to verify it; an env override pointed the provider at a proxy host.
Related errors
- flowxtra: untrusted hostname "${parsed.hostname}" — must be
- gem: invalid URL: ${url}
- gem: URL must use HTTPS: ${url}
- getonbrd: untrusted hostname "${parsed.hostname}" — must be
- glints: untrusted hostname "${parsed.hostname}" — must be on
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/0ada6427eba2b0f1.
Report an issue: GitHub.