{"record":{"id":"3322aabccc6ba045","repo":"santifer/career-ops","slug":"yourator-untrusted-hostname-parsed-hostname","errorCode":null,"errorMessage":"yourator: untrusted hostname \"${parsed.hostname}\" — must be ${TRUSTED_HOST}","messagePattern":"yourator: untrusted hostname \"(.+?)\" — must be (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/yourator.mjs","lineNumber":85,"sourceCode":"const FEED_BASE = `${SITE_ORIGIN}/api/v4/jobs`;\nconst TRUSTED_HOST = 'www.yourator.co';\n// Safety bound only — the loop stops on payload.hasMore. The live board was 88\n// pages on 2026-08-18; this leaves room to grow without silently truncating.\nconst DEFAULT_MAX_PAGES = 120;\nconst MAX_PAGES_CAP = 500;\nconst PAGE_DELAY_MS = 200;\n\n/** @param {string} url */\nfunction assertYouratorUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`yourator: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`yourator: URL must use HTTPS: ${url}`);\n  if (parsed.hostname !== TRUSTED_HOST) {\n    throw new Error(`yourator: untrusted hostname \"${parsed.hostname}\" — must be ${TRUSTED_HOST}`);\n  }\n  return url;\n}\n\n/** Resolve the page cap: a positive integer `max_pages` on the entry, capped. */\nfunction resolveMaxPages(entry) {\n  const v = entry?.max_pages;\n  if (Number.isInteger(v) && v > 0) return Math.min(v, MAX_PAGES_CAP);\n  return DEFAULT_MAX_PAGES;\n}\n\n/**\n * Canonical URL for a posting — Source Indexing Policy rule 2, \"the shortest\n * verifiable path to the employer the source exposes\".\n *\n * Prefers `thirdPartyUrl` (the employer's own ATS page), with the board's\n * `utm_*` ad parameters stripped. Accepts any https: origin — the value is\n * display-only and never fetched here. Falls back to the Yourator posting page","sourceCodeStart":67,"sourceCodeEnd":103,"githubUrl":"https://github.com/santifer/career-ops/blob/1696bec4d021768e7359f9aad6b329cba883da20/providers/yourator.mjs#L67-L103","documentation":"After confirming HTTPS, assertYouratorUrl pins the hostname to TRUSTED_HOST (www.yourator.co, per the provider's documented API at https://www.yourator.co/api/v4/jobs). Any other hostname is rejected to prevent SSRF-style abuse where a crafted config points the fetcher at an internal or attacker-controlled host. The error names the offending hostname and the required one.","triggerScenarios":"assertYouratorUrl receives an https: URL whose parsed.hostname !== TRUSTED_HOST — e.g. https://yourator.co/jobs (missing www.), https://api.yourator.co/v4/jobs, https://evil.example.com/jobs, or a copy of the feed URL served from a mirror domain.","commonSituations":"Omitting the www. prefix because the site resolves either way in a browser; pointing at an API subdomain copied from a blog post; a malicious or typosquatted mirror in config; environment-specific overrides that rewrite the host.","solutions":["Set the hostname to exactly the trusted host: https://www.yourator.co/... (with www.).","Compare `new URL(url).hostname` against 'www.yourator.co' before invoking the provider.","Remove any host-rewriting proxy/mirror from the configuration; the provider only fetches the official domain."],"exampleFix":"// before\ncareers_url: https://yourator.co/jobs\n\n// after\ncareers_url: https://www.yourator.co/jobs","handlingStrategy":"validation","validationCode":"const TRUSTED_HOST = 'www.yourator.co';\nexport function isTrustedYouratorUrl(u) {\n  try { return new URL(u).hostname === TRUSTED_HOST; } catch { return false; }\n}\nif (!isTrustedYouratorUrl(entry.careers_url)) entry.careers_url = 'https://www.yourator.co/jobs';","typeGuard":"function isYouratorUrl(u) {\n  try {\n    const p = new URL(u);\n    return p.protocol === 'https:' && p.hostname === 'www.yourator.co';\n  } catch { return false; }\n}","tryCatchPattern":"try {\n  scanYourator(entry);\n} catch (e) {\n  if (e.message.includes('untrusted hostname')) {\n    console.error(`Bad host in ${entry.careers_url}; defaulting to https://www.yourator.co/jobs`);\n    scanYourator({ ...entry, careers_url: 'https://www.yourator.co/jobs' });\n  } else throw e;\n}","preventionTips":["Use the exact documented URL https://www.yourator.co/jobs — never omit the www. or swap to a subdomain.","Never point provider fetch URLs at proxies or mirrors; host pinning is intentional (SSRF guard).","Unit-test provider config against a hostname allowlist before executing scans."],"tags":["security","ssrf","hostname-validation","configuration"],"backgroundTag":"untrusted-hostname-rejected","analyzedSha":"1696bec4d021768e7359f9aad6b329cba883da20","analyzedAt":"2026-09-01T19:19:23.111Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}