{"record":{"id":"84c91586037dc99d","repo":"santifer/career-ops","slug":"senjob-untrusted-hostname-parsed-hostname","errorCode":null,"errorMessage":"senjob: untrusted hostname \"${parsed.hostname}\" — must be ${TRUSTED_HOST}","messagePattern":"senjob: untrusted hostname \"(.+?)\" — must be (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/senjob.mjs","lineNumber":81,"sourceCode":"const HIDDEN_ISO_DATE_RE = /display:\\s*none;?\\s*\"?>\\s*(\\d{4}-\\d{2}-\\d{2})\\s*</i;\n\n/** @param {any} ctx @param {number} ms */\nfunction sleep(ctx, ms) {\n  if (typeof ctx?.sleep === 'function') return ctx.sleep(ms);\n  return new Promise((r) => setTimeout(r, ms));\n}\n\n/** @param {string} url */\nfunction assertSenjobUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`senjob: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`senjob: URL must use HTTPS: ${url}`);\n  if (parsed.hostname !== TRUSTED_HOST) {\n    throw new Error(`senjob: untrusted hostname \"${parsed.hostname}\" — must be ${TRUSTED_HOST}`);\n  }\n  return url;\n}\n\n/**\n * Collapse a markup fragment to its visible text.\n * Comments are stripped FIRST: the anchor bodies carry `<!-- d ico postulez -->`\n * between the title and a spacer image, and a naive tag strip would leave the\n * comment body sitting inside the title.\n * @param {string} fragment\n * @returns {string}\n */\nexport function visibleText(fragment) {\n  return decodeEntities(\n    String(fragment ?? '')\n      .replace(/<!--[\\s\\S]*?-->/g, ' ')\n      .replace(/<[^>]+>/g, ' '),\n  )","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/santifer/career-ops/blob/1696bec4d021768e7359f9aad6b329cba883da20/providers/senjob.mjs#L63-L99","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nassertSenjobUrl('https://www.senjob.com/offres-d-emploi.php');\n// after\nassertSenjobUrl('https://senjob.com/offres-d-emploi.php');","handlingStrategy":"validation","validationCode":"const TRUSTED_HOST = 'senjob.com';\nfunction isSenjobHostUrl(url) {\n  try { return new URL(url).hostname === TRUSTED_HOST; } catch { return false; }\n}\nif (!isSenjobHostUrl(url)) throw new Error(`refusing non-senjob.com host: ${url}`);","typeGuard":"function isSenjobHostUrl(url) {\n  try { return new URL(url).hostname === 'senjob.com'; } catch { return false; }\n}","tryCatchPattern":"try {\n  assertSenjobUrl(url);\n} catch (err) {\n  if (String(err.message).startsWith('senjob: untrusted hostname')) {\n    console.error(`Host pinning refused ${url} — only senjob.com (exact) is allowed`);\n    return null;\n  }\n  throw err;\n}","preventionTips":["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"],"tags":["url-validation","ssrf-protection","host-pinning"],"backgroundTag":"untrusted-hostname","analyzedSha":"1696bec4d021768e7359f9aad6b329cba883da20","analyzedAt":"2026-09-01T19:19:23.111Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}