{"record":{"id":"501a10bd3be7336a","repo":"santifer/career-ops","slug":"agentic-jobs-untrusted-hostname-parsed-hostnam","errorCode":null,"errorMessage":"agentic-jobs: untrusted hostname \"${parsed.hostname}\" — must be ${TRUSTED_HOST}","messagePattern":"agentic-jobs: untrusted hostname \"(.+?)\" — must be (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/agentic-jobs.mjs","lineNumber":49,"sourceCode":"const SITE_ORIGIN = 'https://agentic-engineering-jobs.com';\nconst API_BASE = `${SITE_ORIGIN}/api/v1`;\nconst TRUSTED_HOST = 'agentic-engineering-jobs.com';\nconst PAGE_SIZE = 50; // fixed by the API (meta.per_page)\nconst MAX_PAGES = 40; // safety cap on request count (40*50 = 2000 postings)\nconst MAX_JOBS = 2000;\nconst PAGE_DELAY_MS = 2100; // stays under the documented 30 req/60s limit\n\n/** @param {string} url */\nfunction assertAgenticUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`agentic-jobs: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`agentic-jobs: URL must use HTTPS: ${url}`);\n  if (parsed.hostname !== TRUSTED_HOST) {\n    throw new Error(`agentic-jobs: untrusted hostname \"${parsed.hostname}\" — must be ${TRUSTED_HOST}`);\n  }\n  return url;\n}\n\nconst regionNames = new Intl.DisplayNames(['en'], { type: 'region' });\n\n/**\n * Resolve a two-letter ISO country code to an English name. Returns '' for\n * anything that isn't a resolvable two-letter code. Exported for tests.\n * @param {unknown} code\n */\nexport function countryName(code) {\n  if (typeof code !== 'string' || !/^[A-Za-z]{2}$/.test(code)) return '';\n  try {\n    const name = regionNames.of(code.toUpperCase());\n    return name && name !== code.toUpperCase() ? name : '';\n  } catch {\n    return '';","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/agentic-jobs.mjs#L31-L67","documentation":"assertAgenticUrl validates every URL the agentic-jobs provider will request as an SSRF guard: it must parse, be HTTPS, and its hostname must equal the hardcoded TRUSTED_HOST ('agentic-engineering-jobs.com'). This throw fires when a URL pointing at a different host is about to be fetched, refusing to follow a redirect or a misconfigured base to an arbitrary server. In practice the provider builds all URLs from API_BASE internally, so hitting this means something substituted a foreign URL into the fetch path.","triggerScenarios":"assertAgenticUrl is called with a URL whose hostname differs from agentic-engineering-jobs.com — e.g. http-vs-https variants were rejected earlier, so this is a genuinely different host: an overridden API_BASE, a test passing a mock URL like https://localhost/api/v1/jobs, or a redirect target that was passed back through the validator instead of following the redirect.","commonSituations":"Test harnesses stubbing the API with a local server URL; forks or patches pointing API_BASE at a mirror or proxy; someone wiring a custom portal entry whose URL the provider re-validates; DNS/hosts-file tricks that don't apply here because validation is on the hostname string, not resolution.","solutions":["Use the built-in provider unchanged — it constructs URLs from API_BASE itself; do not pass custom URLs into assertAgenticUrl.","If testing, stub ctx.fetchJson rather than pointing the validator at a local host, or inject the mock at the fetch layer.","If you intentionally need a different endpoint (mirror/proxy), edit TRUSTED_HOST/API_BASE in providers/agentic-jobs.mjs deliberately — never bypass the check by catching and proceeding.","Check the URL that reached the validator: if it's a redirect target, the provider uses redirect:'error' by design; do not feed redirects back through assertAgenticUrl."],"exampleFix":"// before (test)\nconst url = 'http://localhost:3000/api/v1/jobs?page=1';\nassertAgenticUrl(url); // throws: untrusted hostname\n// after (stub at the fetch layer instead)\nconst ctx = { fetchJson: async () => ({ data: [], meta: { total: 0, per_page: 50 } }) };\nawait provider.fetch(entry, ctx);","handlingStrategy":"validation","validationCode":"const u = new URL(candidateUrl);\nif (u.protocol !== 'https:' || u.hostname !== 'agentic-engineering-jobs.com') {\n  throw new Error(`refusing to fetch untrusted URL: ${candidateUrl}`);\n}","typeGuard":"function isTrustedAgenticUrl(url) {\n  try { const u = new URL(url); return u.protocol === 'https:' && u.hostname === 'agentic-engineering-jobs.com'; }\n  catch { return false; }\n}","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).includes('untrusted hostname')) {\n    console.error('URL was redirected/overridden away from the trusted host — check API_BASE overrides and test stubs');\n  }\n  throw err;\n}","preventionTips":["Never override API_BASE or pass external URLs into the provider's URL validator.","Stub ctx.fetchJson in tests instead of pointing the provider at localhost.","Treat this error as a security signal: something redirected or rewrote the target host — investigate rather than bypassing."],"tags":["ssrf","url-validation","security","provider"],"backgroundTag":"invalid-url","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"}