{"record":{"id":"58dc042d93f9116e","repo":"santifer/career-ops","slug":"themuse-untrusted-hostname-parsed-hostname","errorCode":null,"errorMessage":"themuse: untrusted hostname \"${parsed.hostname}\" — must be ${TRUSTED_HOST}","messagePattern":"themuse: untrusted hostname \"(.+?)\" — must be (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/themuse.mjs","lineNumber":92,"sourceCode":"      const retryAfterMs = parseRetryAfterMs(err?.retryAfter);\n      const delayMs = retryAfterMs !== null ? Math.min(retryAfterMs, RETRY_MAX_DELAY_MS * 4) : (backoff + Math.random() * 250);\n      await sleep(delayMs, ctx);\n    }\n  }\n  throw lastErr;\n}\n\n/** @param {string} url */\nfunction assertMuseUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`themuse: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`themuse: URL must use HTTPS: ${url}`);\n  if (parsed.hostname !== TRUSTED_HOST) {\n    throw new Error(`themuse: untrusted hostname \"${parsed.hostname}\" — must be ${TRUSTED_HOST}`);\n  }\n  return url;\n}\n\n/**\n * Normalize a single result from the Muse API response. Exported for unit tests.\n *\n * Field mapping:\n *   name              → title\n *   refs.landing_page → url\n *   company.name      → company\n *   locations[0].name → location\n *\n * Returns null when required fields (title or url) are missing or invalid.\n *\n * @param {any} j\n * @returns {{ title: string, url: string, company: string, location: string } | null}\n */","sourceCodeStart":74,"sourceCodeEnd":110,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/themuse.mjs#L74-L110","documentation":"assertMuseUrl() allow-lists a single trusted hostname (TRUSTED_HOST). Even a valid HTTPS URL is rejected if its hostname is anything other than the Muse domain. This SSRF-style guard stops crafted or misconfigured URLs from redirecting the provider at arbitrary hosts.","triggerScenarios":"assertMuseUrl(url) receives a valid https URL whose parsed.hostname !== TRUSTED_HOST — e.g. a mirror domain, a subdomain typo (themuse.co vs themuse.com), or an attacker-controlled host injected via config.","commonSituations":"A typo like 'www.themuse.org' or a leading 'api.' added by hand; a regional mirror pasted into portals.yml; a template variable resolving to the wrong host; someone attempting to point the provider at an internal host.","solutions":["Correct the hostname in the config to exactly the trusted Muse host the provider expects.","Check the TRUSTED_HOST constant in providers/themuse.mjs and match your URL against it character-for-character (no subdomain additions).","Remove mirror/lookalike entries from portals.yml — only the official domain is fetchable by design.","If a new legitimate host must be supported, extend TRUSTED_HOST to an allow-list in a reviewed change, not ad hoc."],"exampleFix":"// before\nurl: https://api.themuse.com/v2/jobs?page=0\n// after\nurl: https://www.themuse.com/api/v2/jobs?page=0","handlingStrategy":"validation","validationCode":"const TRUSTED_MUSE_HOST = 'www.themuse.com'; // match the provider's TRUSTED_HOST\nfunction isTrustedMuseUrl(value) {\n  try {\n    const u = new URL(value);\n    return u.protocol === 'https:' && u.hostname === TRUSTED_MUSE_HOST;\n  } catch { return false; }\n}\n// filter entries: entries.filter(e => !isTrustedMuseUrl(e.url)) → correct before scanning","typeGuard":"const isMuseHost = (v) => {\n  try { return new URL(v).hostname === 'www.themuse.com'; } catch { return false; }\n};","tryCatchPattern":"try {\n  return await provider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).includes('untrusted hostname')) {\n    console.warn(`${entry.name} points at a non-Muse host; remove or correct the URL`);\n    return null;\n  }\n  throw err;\n}","preventionTips":["Copy portal URLs from the provider's own docs/examples rather than hand-typing hostnames","Keep the trusted-host constant as the single source of truth and validate config against it in CI","Never add mirror or lookalike domains — treat the allow-list as intentional and reviewed","Watch for template variables silently substituting the wrong hostname into URLs"],"tags":["url","ssrf","allowlist","themuse","security"],"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"}