santifer/career-ops · error · Error

pythonorg: untrusted hostname

Error message

pythonorg: untrusted hostname "${parsed.hostname}" — must be ${TRUSTED_HOST}

What it means

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.

Solutions

  1. Use the official feed host: 'https://www.python.org/jobs/feed/rss/' (www.python.org is trusted as a .python.org subdomain)
  2. Fix the config entry's hostname to be on python.org; other hosts belong to a different provider/entry
  3. 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
  4. Double-check for lookalike domains (pythonorg.org, python-org.com) — the endsWith('.python.org') check rejects them by design

Example fix

// before
assertPythonOrgUrl('https://python.org.example.com/jobs/feed/rss/'); // untrusted
// after
assertPythonOrgUrl('https://www.python.org/jobs/feed/rss/');
Defensive patterns

Strategy: validation

Validate before calling

function isTrustedPythonOrgHost(url) {
  try {
    const u = new URL(url);
    return u.protocol === 'https:' &&
      (u.hostname.toLowerCase() === 'python.org' || u.hostname.toLowerCase().endsWith('.python.org'));
  } catch { return false; }
}

Type guard

function isPythonOrgUrl(value) {
  if (typeof value !== 'string') return false;
  try {
    const h = new URL(value).hostname.toLowerCase();
    return h === 'python.org' || h.endsWith('.python.org');
  } catch { return false; }
}

Try / catch

try {
  assertPythonOrgUrl(cfg.feedUrl);
} catch (e) {
  if (e.message.startsWith('pythonorg: untrusted hostname')) {
    console.error(`Security: refusing non-python.org host in feedUrl — ${e.message}`);
  } else throw e;
}

Prevention

When it happens

Trigger: 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/...'.

Common situations: 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.

Understand the failure class

Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.

Related errors


AI-assisted analysis of santifer/career-ops@e7abd431fc (2026-09-22). Data as JSON: /api/errors/3fbe609071864214. Report an issue: GitHub.

Appendix: source

Thrown at providers/pythonorg.mjs:32

//
// Wire in via a `job_boards:` entry with `provider: pythonorg`.

const FEED_URL = 'https://www.python.org/jobs/feed/rss/';
const TRUSTED_HOST = 'python.org';

/** @param {string} url */
export function assertPythonOrgUrl(url) {
  let parsed;
  try {
    parsed = new URL(url);
  } catch {
    throw new Error(`pythonorg: invalid URL: ${url}`);
  }
  if (parsed.protocol !== 'https:') throw new Error(`pythonorg: URL must use HTTPS: ${url}`);
  const host = parsed.hostname.toLowerCase();
  const trusted = host === TRUSTED_HOST || host.endsWith(`.${TRUSTED_HOST}`);
  if (!trusted) {
    throw new Error(`pythonorg: untrusted hostname "${parsed.hostname}" — must be ${TRUSTED_HOST}`);
  }
  return url;
}

// NaN-safe Date.parse — `|| undefined` would also coerce a valid epoch 0.
function toEpochMs(value) {
  if (!value) return undefined;
  const parsed = Date.parse(value);
  return Number.isNaN(parsed) ? undefined : parsed;
}

function fallbackCompany(entry) {
  return typeof entry?.name === 'string' && entry.name.trim() ? entry.name.trim() : 'Python.org';
}

// Resolve a tag's inner text: unwrap a CDATA section, else decode entities.
function extractText(inner) {
  const cdata = inner.match(/^\s*<!\[CDATA\[([\s\S]*?)\]\]>\s*$/);

View on GitHub (pinned to e7abd431fc)