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
- 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
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
- 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
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
- landingjobs: untrusted hostname
- larajobs: untrusted hostname
- lever: untrusted hostname
- torre: untrusted hostname
- weworkremotely: untrusted hostname
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)