santifer/career-ops · error · Error
senjob: untrusted hostname "${parsed.hostname}" — must be ${
Error message
senjob: untrusted hostname "${parsed.hostname}" — must be ${TRUSTED_HOST} What it means
All senjob requests are host-pinned to senjob.com (TRUSTED_HOST). assertSenjobUrl throws this error when the parsed hostname is anything else, including subdomains like www.senjob.com (the check is exact equality, not suffix matching). This prevents SSRF and stops the scraper from following a repointed domain.
Source
Thrown at providers/senjob.mjs:81
const HIDDEN_ISO_DATE_RE = /display:\s*none;?\s*"?>\s*(\d{4}-\d{2}-\d{2})\s*</i;
/** @param {any} ctx @param {number} ms */
function sleep(ctx, ms) {
if (typeof ctx?.sleep === 'function') return ctx.sleep(ms);
return new Promise((r) => setTimeout(r, ms));
}
/** @param {string} url */
function assertSenjobUrl(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
throw new Error(`senjob: invalid URL: ${url}`);
}
if (parsed.protocol !== 'https:') throw new Error(`senjob: URL must use HTTPS: ${url}`);
if (parsed.hostname !== TRUSTED_HOST) {
throw new Error(`senjob: untrusted hostname "${parsed.hostname}" — must be ${TRUSTED_HOST}`);
}
return url;
}
/**
* Collapse a markup fragment to its visible text.
* Comments are stripped FIRST: the anchor bodies carry `<!-- d ico postulez -->`
* between the title and a spacer image, and a naive tag strip would leave the
* comment body sitting inside the title.
* @param {string} fragment
* @returns {string}
*/
export function visibleText(fragment) {
return decodeEntities(
String(fragment ?? '')
.replace(/<!--[\s\S]*?-->/g, ' ')
.replace(/<[^>]+>/g, ' '),
)View on GitHub (pinned to 1696bec4d0)
Solutions
- Use the exact host senjob.com — build URLs with buildListUrl(page) so the host is fixed
- Do not pre-resolve redirects; the provider fetches with redirect:'error' and asserts before fetching
- If the board moves to another canonical host, update TRUSTED_HOST/LIST_URL in providers/senjob.mjs and re-verify
Example fix
// before
assertSenjobUrl('https://www.senjob.com/offres-d-emploi.php');
// after
assertSenjobUrl('https://senjob.com/offres-d-emploi.php'); Defensive patterns
Strategy: validation
Validate before calling
const TRUSTED_HOST = 'senjob.com';
function isSenjobHostUrl(url) {
try { return new URL(url).hostname === TRUSTED_HOST; } catch { return false; }
}
if (!isSenjobHostUrl(url)) throw new Error(`refusing non-senjob.com host: ${url}`); Type guard
function isSenjobHostUrl(url) {
try { return new URL(url).hostname === 'senjob.com'; } catch { return false; }
} Try / catch
try {
assertSenjobUrl(url);
} catch (err) {
if (String(err.message).startsWith('senjob: untrusted hostname')) {
console.error(`Host pinning refused ${url} — only senjob.com (exact) is allowed`);
return null;
}
throw err;
} Prevention
- The hostname check is exact equality — www.senjob.com and subdomains are NOT accepted; use bare senjob.com
- Never follow redirects to another host before validation; the provider pairs this guard with fetch redirect:'error'
- Build URLs via buildListUrl(page) so the trusted host is baked in
- If the board's canonical host changes, update TRUSTED_HOST deliberately and re-verify robots.txt and parsing, not just the string
When it happens
Trigger: assertSenjobUrl called with a URL on another host: www.senjob.com, a mirror/staging host, a typosquatted domain, or a URL rewritten from a redirect target. Exact-match means even legitimate subdomains are refused.
Common situations: Assuming www.senjob.com is equivalent to senjob.com; configuring a national variant domain; custom code following an http redirect to another host before assertion.
Related errors
- rippling: untrusted hostname "${parsed.hostname}" — must be
- 4dayweek: invalid URL: ${url}
- 4dayweek: URL must use HTTPS: ${url}
- a16z-speedrun-talent: invalid URL: ${url}
- a16z-speedrun-talent: URL must use HTTPS: ${url}
AI-assisted analysis of santifer/career-ops@1696bec4d0 (2026-09-01).
Data as JSON: /api/errors/84c91586037dc99d.
Report an issue: GitHub.