{"record":{"id":"f6873f68ae0d3dde","repo":"santifer/career-ops","slug":"wttj-untrusted-label-hostname-parsed-hostna","errorCode":null,"errorMessage":"wttj: untrusted ${label} hostname \"${parsed.hostname}\" — must be ${host}","messagePattern":"wttj: untrusted (.+?) hostname \"(.+?)\" — must be (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/wttj.mjs","lineNumber":67,"sourceCode":"// manager\" alone returns ~14k hits — so Algolia's own relevance ranking, not the\n// scanner's filters, decides which 200 are seen. A server-side `filters`\n// expression cuts the result set to something a single request can actually\n// exhaust (e.g. product-management + France + full_time is ~450), so the cap is\n// raised to Algolia's per-request ceiling for this index when one is configured.\nconst FILTERED_MAX_HITS_CAP = 1000;\nconst FILTERS_MAX_LEN = 1000;\n\n/** Pin a URL to an expected https host. */\nfunction assertHost(url, host, label) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`wttj: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`wttj: URL must use HTTPS: ${url}`);\n  if (parsed.hostname !== host.toLowerCase()) {\n    throw new Error(`wttj: untrusted ${label} hostname \"${parsed.hostname}\" — must be ${host}`);\n  }\n  return url;\n}\n\n/**\n * Parse the `window.env = {...}` payload served by /api/env and extract the\n * Algolia application id + client search key.\n * @param {string} text\n * @returns {{ appId: string, apiKey: string }}\n */\nexport function parseEnvPayload(text) {\n  const start = text.indexOf('{');\n  const end = text.lastIndexOf('}');\n  if (start === -1 || end <= start) throw new Error('wttj: /api/env payload has no JSON object');\n  let env;\n  try {\n    env = JSON.parse(text.slice(start, end + 1));\n  } catch {","sourceCodeStart":49,"sourceCodeEnd":85,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/wttj.mjs#L49-L85","documentation":"assertHost's final check compares parsed.hostname (case-insensitively) against the exact expected host parameter. Any other hostname — a lookalike domain, a redirect target, or a typo — is rejected to keep fetches pinned to the legitimate WTTJ/ATS host.","triggerScenarios":"assertHost(url, host, label) where url parses to https but hostname differs from host, e.g. assertHost('https://evil.io/api/env', 'www.wttj.fr', 'env') or 'https://www.wttj.fr.evil.io/api/env'.","commonSituations":"A careers_url pointing at the company site rather than the WTTJ board domain; an attacker-shaped host in config; passing the wrong host argument order to assertHost.","solutions":["Correct the URL to use the expected host named in the error message","Check for typos or appended domains in the hostname","If you passed the wrong host constant to assertHost, fix the call site"],"exampleFix":"// before\nassertHost('https://jobs.welcome-to-the-jungle.com/...', 'www.wttj.fr', 'env');\n// after\nassertHost('https://www.wttj.fr/api/env', 'www.wttj.fr', 'env');","handlingStrategy":"validation","validationCode":"function hostIs(u, expected) { try { return new URL(u).hostname === expected.toLowerCase(); } catch { return false; } }\nif (!hostIs(entry.careers_url, 'www.wttj.fr')) throw new Error(`unexpected host in ${entry.careers_url}`);","typeGuard":"const hostIs = (u, expected) => { try { return new URL(u).hostname === expected.toLowerCase(); } catch { return false; } };","tryCatchPattern":"try { return await wttj.fetch(entry, ctx); } catch (e) { if (e.message.includes('untrusted')) { console.warn(`${entry.name}: hostname not pinned to ${e.message.match(/must be (\\S+)/)?.[1]}`); return []; } throw e; }","preventionTips":["Compare hostnames (not hrefs) when validating configured URLs","Beware lookalike domains when copying careers URLs","Keep the expected host constants next to the assertHost call sites and review changes"],"tags":["ssrf-guard","hostname-allowlist","wttj","security"],"backgroundTag":"untrusted-hostname","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}