{"record":{"id":"4bc2e5b6af01af44","repo":"santifer/career-ops","slug":"manfred-untrusted-hostname-parsed-hostname","errorCode":null,"errorMessage":"manfred: untrusted hostname \"${parsed.hostname}\" — must be ${TRUSTED_HOST}","messagePattern":"manfred: untrusted hostname \"(.+?)\" — must be (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/manfred.mjs","lineNumber":45,"sourceCode":"// so any network jitter aborts it. Give it real headroom rather than relying\n// on retry alone to paper over a structurally near-timeout request.\nconst FETCH_TIMEOUT_MS = 25_000;\nconst TRUSTED_HOST = 'www.getmanfred.com';\nconst OFFER_BASE = 'https://www.getmanfred.com/ofertas-empleo';\nconst VALID_LANGS = ['EN', 'ES'];\nconst DEFAULT_LANG = 'EN';\n\n/** @param {string} url */\nfunction assertManfredUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`manfred: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`manfred: URL must use HTTPS: ${url}`);\n  if (parsed.hostname !== TRUSTED_HOST) {\n    throw new Error(`manfred: untrusted hostname \"${parsed.hostname}\" — must be ${TRUSTED_HOST}`);\n  }\n  return url;\n}\n\n/** Resolve the feed language: `lang` on the entry, uppercased, else EN. */\nexport function resolveLang(entry) {\n  const raw = typeof entry?.lang === 'string' ? entry.lang.trim().toUpperCase() : '';\n  return VALID_LANGS.includes(raw) ? raw : DEFAULT_LANG;\n}\n\n// The feed reports currency as the SYMBOL, not an ISO code, and the observed\n// values include a narrow-no-break-space variant of the euro sign. scan.mjs's\n// salary_filter compares currencies case-insensitively as plain strings, so a\n// symbol would never match a user's `currency: EUR` — map to ISO, and drop the\n// field entirely rather than guess when the symbol is unknown.\nconst CURRENCY_BY_SYMBOL = new Map([\n  ['€', 'EUR'],\n  ['£', 'GBP'],","sourceCodeStart":27,"sourceCodeEnd":63,"githubUrl":"https://github.com/santifer/career-ops/blob/e7abd431fce9348a95261acac9e0c14779c35df8/providers/manfred.mjs#L27-L63","documentation":"assertManfredUrl() pins the hostname to a single trusted host (TRUSTED_HOST, getmanfred.com). Any other hostname is rejected with this error, which names both the offending hostname and the required one. This prevents SSRF-style misuse where a crafted config points the fetcher at an arbitrary server.","triggerScenarios":"A careers_url or feed URL pointing at a look-alike or mirror host (e.g. https://getmanfred.net, https://api.getmanfred.com, https://evil.example) instead of the exact trusted host; a proxy hostname substituted in config.","commonSituations":"Using a CDN or regional mirror of the Manfred feed; routing through a corporate proxy by rewriting the host; a typo in the domain (.io vs .com); intentionally pointing at a mock server whose host isn't the pinned one.","solutions":["Point the URL at the exact trusted host getmanfred.com (matching TRUSTED_HOST in providers/manfred.mjs).","If you must test against a local/mock server, inject a fetch context (ctx.fetchJson) in tests instead of changing the hostname.","If the trusted host has legitimately changed, update TRUSTED_HOST in the provider source deliberately — not by editing the URL alone."],"exampleFix":"// before\ncareers_url: https://api.getmanfred.com/feed\n// after\ncareers_url: https://getmanfred.com/feed","handlingStrategy":"validation","validationCode":"const TRUSTED = 'getmanfred.com';\nconst u = new URL(entry.careers_url);\nif (u.hostname !== TRUSTED) throw new Error(`${entry.name}: ${u.hostname} is not the trusted host ${TRUSTED}`);","typeGuard":"const isTrustedHost = (v, host) => { try { return new URL(v).hostname === host; } catch { return false; } };","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).startsWith('manfred: untrusted hostname')) {\n    console.error('Point the entry back at the pinned host; mirrors/proxies are not allowed');\n    return;\n  }\n  throw err;\n}","preventionTips":["Never substitute mirror/CDN hosts for the pinned provider host in config.","For tests, mock ctx.fetchJson instead of repointing the hostname.","After a provider domain change, update TRUSTED_HOST in the provider source deliberately and review the diff."],"tags":["url","security","ssrf","hostname-pinning"],"backgroundTag":"invalid-url","analyzedSha":"e7abd431fce9348a95261acac9e0c14779c35df8","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}