{"record":{"id":"3fbe609071864214","repo":"santifer/career-ops","slug":"pythonorg-untrusted-hostname-parsed-hostname-must-be-trusted","errorCode":null,"errorMessage":"pythonorg: untrusted hostname \"${parsed.hostname}\" — must be ${TRUSTED_HOST}","messagePattern":"pythonorg: untrusted hostname \"(.+?)\" — must be (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/pythonorg.mjs","lineNumber":32,"sourceCode":"//\n// Wire in via a `job_boards:` entry with `provider: pythonorg`.\n\nconst FEED_URL = 'https://www.python.org/jobs/feed/rss/';\nconst TRUSTED_HOST = 'python.org';\n\n/** @param {string} url */\nexport function assertPythonOrgUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`pythonorg: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`pythonorg: URL must use HTTPS: ${url}`);\n  const host = parsed.hostname.toLowerCase();\n  const trusted = host === TRUSTED_HOST || host.endsWith(`.${TRUSTED_HOST}`);\n  if (!trusted) {\n    throw new Error(`pythonorg: untrusted hostname \"${parsed.hostname}\" — must be ${TRUSTED_HOST}`);\n  }\n  return url;\n}\n\n// NaN-safe Date.parse — `|| undefined` would also coerce a valid epoch 0.\nfunction toEpochMs(value) {\n  if (!value) return undefined;\n  const parsed = Date.parse(value);\n  return Number.isNaN(parsed) ? undefined : parsed;\n}\n\nfunction fallbackCompany(entry) {\n  return typeof entry?.name === 'string' && entry.name.trim() ? entry.name.trim() : 'Python.org';\n}\n\n// Resolve a tag's inner text: unwrap a CDATA section, else decode entities.\nfunction extractText(inner) {\n  const cdata = inner.match(/^\\s*<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>\\s*$/);","sourceCodeStart":14,"sourceCodeEnd":50,"githubUrl":"https://github.com/santifer/career-ops/blob/e7abd431fce9348a95261acac9e0c14779c35df8/providers/pythonorg.mjs#L14-L50","documentation":"assertPythonOrgUrl enforces a trusted-host allowlist: the hostname must be exactly python.org or any subdomain ending in .python.org (case-insensitive). URLs pointing at any other host are rejected with the untrusted-hostname error, preventing the provider from being pointed at a lookalike or third-party mirror. The message names the offending hostname and the required trust root.","triggerScenarios":"Calling assertPythonOrgUrl with a parseable https: URL whose hostname is not python.org or a *.python.org subdomain — e.g. 'https://evil.example.com/jobs/feed/rss/', 'https://python.org.example.com/...', or a typo'd host like 'https://pythonorg.org/...'.","commonSituations":"Config entry pointing at a mirror or proxy; a phishing/SSRF attempt supplying a lookalike host; subdomain-style mistakes like python.org.example.com; copying a URL from a different job board (e.g. the JS jobs feed) into a pythonorg entry.","solutions":["Use the official feed host: 'https://www.python.org/jobs/feed/rss/' (www.python.org is trusted as a .python.org subdomain)","Fix the config entry's hostname to be on python.org; other hosts belong to a different provider/entry","If a proxy mirror is genuinely needed, that requires changing the provider's TRUSTED_HOST allowlist — treat as a code change, not a config fix","Double-check for lookalike domains (pythonorg.org, python-org.com) — the endsWith('.python.org') check rejects them by design"],"exampleFix":"// before\nassertPythonOrgUrl('https://python.org.example.com/jobs/feed/rss/'); // untrusted\n// after\nassertPythonOrgUrl('https://www.python.org/jobs/feed/rss/');","handlingStrategy":"validation","validationCode":"function isTrustedPythonOrgHost(url) {\n  try {\n    const u = new URL(url);\n    return u.protocol === 'https:' &&\n      (u.hostname.toLowerCase() === 'python.org' || u.hostname.toLowerCase().endsWith('.python.org'));\n  } catch { return false; }\n}","typeGuard":"function isPythonOrgUrl(value) {\n  if (typeof value !== 'string') return false;\n  try {\n    const h = new URL(value).hostname.toLowerCase();\n    return h === 'python.org' || h.endsWith('.python.org');\n  } catch { return false; }\n}","tryCatchPattern":"try {\n  assertPythonOrgUrl(cfg.feedUrl);\n} catch (e) {\n  if (e.message.startsWith('pythonorg: untrusted hostname')) {\n    console.error(`Security: refusing non-python.org host in feedUrl — ${e.message}`);\n  } else throw e;\n}","preventionTips":["Only configure URLs on the provider's own host (python.org or *.python.org); mirrors/proxies belong in a different entry type","Beware lookalike/subdomain-injected hosts (python.org.example.com) — the endsWith check rejects them; keep that behavior","Review any config change that alters a provider URL as a security-relevant change (SSRF surface)","Use the canonical feed URL constant instead of free-form strings wherever possible"],"tags":["url-validation","security","ssrf","allowlist","pythonorg"],"backgroundTag":"invalid-url","analyzedSha":"e7abd431fce9348a95261acac9e0c14779c35df8","analyzedAt":"2026-09-22T13:19:01.448Z","contentChangedAt":"2026-09-22T13:19:01.448Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}