{"record":{"id":"cb468dfcdc20e1f9","repo":"santifer/career-ops","slug":"jobspresso-untrusted-hostname-parsed-hostname","errorCode":null,"errorMessage":"jobspresso: untrusted hostname \"${parsed.hostname}\" - must be ${TRUSTED_HOST}","messagePattern":"jobspresso: untrusted hostname \"(.+?)\" - must be (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/jobspresso.mjs","lineNumber":25,"sourceCode":"// approach as providers/personio.mjs rather than adding an XML dependency.\n//\n// Wire in via a `job_boards:` entry with `provider: jobspresso`.\n\nconst FEED_URL = 'https://jobspresso.co/?feed=job_feed';\nconst TRUSTED_HOST = 'jobspresso.co';\n\n/** @param {string} url */\nfunction assertJobspressoUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`jobspresso: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== \"https:\")\n    throw new Error(`jobspresso: URL must use HTTPS: ${url}`);\n  if (parsed.hostname !== TRUSTED_HOST) {\n    throw new Error(\n      `jobspresso: untrusted hostname \"${parsed.hostname}\" - must be ${TRUSTED_HOST}`,\n    );\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\n/** @type {Provider} */\nexport default {\n  id: \"jobspresso\",\n\n  detect(entry) {","sourceCodeStart":7,"sourceCodeEnd":43,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/jobspresso.mjs#L7-L43","documentation":"Thrown by assertJobspressoUrl when the URL's hostname is not exactly jobspresso.co (strict equality, no subdomain tolerance). This is the core SSRF allowlist guard. The shipped provider only passes the hardcoded jobspresso.co constant, so it cannot fire unless the constant or the function's input changes.","triggerScenarios":"Editing FEED_URL to a different host (a CDN/mirror); reusing assertJobspressoUrl on entry input pointing at a subdomain like feed.jobspresso.co; an attempt to route the request off-host via a crafted URL.","commonSituations":"Switching to a subdomain endpoint and assuming the assert allows it; a fork adding a configurable host without widening the allowlist; malicious/typo entry input routed through the assert.","solutions":["Keep FEED_URL on the bare jobspresso.co host, or if a subdomain is genuinely required, update the assert to allow *.jobspresso.co.","Never route untrusted entry input through assertJobspressoUrl without confirming the host.","Document any allowlist change next to TRUSTED_HOST."],"exampleFix":"// before (strict — rejects subdomains)\nif (parsed.hostname !== TRUSTED_HOST) { throw ... }\n\n// after (allow subdomains)\nif (parsed.hostname !== TRUSTED_HOST && !parsed.hostname.endsWith('.' + TRUSTED_HOST)) { throw ... }","handlingStrategy":"validation","validationCode":"const TRUSTED_HOST = 'jobspresso.co';\nfunction isTrustedJobspressoHost(url) {\n  const u = new URL(url);\n  return u.hostname === TRUSTED_HOST; // assert is strict — subdomains NOT allowed\n}","typeGuard":"/** True for a URL on exactly jobspresso.co (no subdomains, per the assert). */\nfunction isJobspressoTrusted(url) {\n  try {\n    const u = new URL(url);\n    return u.protocol === 'https:' && u.hostname === 'jobspresso.co';\n  } catch { return false; }\n}","tryCatchPattern":"try {\n  return await jobspressoProvider.fetch(entry, ctx);\n} catch (err) {\n  if (/untrusted hostname/.test(err.message)) {\n    console.error(`jobspresso: host not allowlisted — ${err.message}`);\n  }\n  throw err;\n}","preventionTips":["Keep FEED_URL on the bare jobspresso.co host; widen the allowlist deliberately if a subdomain is truly needed.","Treat the SSRF allowlist as load-bearing — never widen it casually.","Do not route untrusted entry input through the assert."],"tags":["ssrf","url-validation","allowlist","jobspresso","defense-in-depth"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}