{"record":{"id":"f49a7c840832d032","repo":"santifer/career-ops","slug":"jobstreet-untrusted-hostname-parsed-hostname","errorCode":null,"errorMessage":"jobstreet: untrusted hostname \"${parsed.hostname}\" — must be one of: ${[...ALLOWED_JOBSTREET_HOSTS].join(', ')}","messagePattern":"jobstreet: untrusted hostname \"(.+?)\" — must be one of: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/jobstreet.mjs","lineNumber":86,"sourceCode":"\n/** @param {string} origin — scheme + hostname */\nfunction jobDetailPath(origin) {\n  let host = '';\n  try { host = new URL(origin).hostname; } catch { /* fall through to the common path */ }\n  return ID_LOCALE_HOSTS.has(host) ? '/id/job/' : '/job/';\n}\n\n/** @param {string} url */\nfunction assertJobstreetUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`jobstreet: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`jobstreet: URL must use HTTPS: ${url}`);\n  if (!ALLOWED_JOBSTREET_HOSTS.has(parsed.hostname))\n    throw new Error(`jobstreet: untrusted hostname \"${parsed.hostname}\" — must be one of: ${[...ALLOWED_JOBSTREET_HOSTS].join(', ')}`);\n  return url;\n}\n\n/**\n * Derive the origin from the API hostname.\n * e.g. id.jobstreet.com → https://id.jobstreet.com\n * @param {string} apiUrl\n * @returns {string}\n */\nfunction deriveOrigin(apiUrl) {\n  try {\n    const parsed = new URL(apiUrl);\n    return `${parsed.protocol}//${parsed.hostname}`;\n  } catch {\n    return 'https://id.jobstreet.com';\n  }\n}\n","sourceCodeStart":68,"sourceCodeEnd":104,"githubUrl":"https://github.com/santifer/career-ops/blob/e7abd431fce9348a95261acac9e0c14779c35df8/providers/jobstreet.mjs#L68-L104","documentation":"assertJobstreetUrl() only accepts hostnames in ALLOWED_JOBSTREET_HOSTS (id/www.jobstreet.com, jobstreet.com/.co.id, sg/my.jobstreet.com, hk.jobsdb.com, www.seek.com.au, www.seek.co.nz). A parsed URL whose hostname is not in that set throws this error, blocking SSRF via the configurable `api:` URL.","triggerScenarios":"`provider: jobstreet` with `api:` pointing at a different host — e.g. a company's own domain, a proxy like localhost, a typo like jobstreet.com.au, or x.jobstreet.com.evil.io.","commonSituations":"Typing a regional variant not on the allowlist (au.jobstreet.com, th.jobsdb.com); pointing api: at a corporate gateway; a typo squatted hostname after a copy-paste.","solutions":["Set `api:` to one of the allowlisted hosts — e.g. https://id.jobstreet.com or https://www.seek.com.au depending on the market.","Pick the matching siteKey for that market (ID-Main, SG-Main, MY-Main, HK-Main) instead of changing the host.","If you genuinely need another SEEK-platform host, add it to ALLOWED_JOBSTREET_HOSTS in providers/jobstreet.mjs after confirming it is the real SEEK infrastructure."],"exampleFix":"// before (portals.yml)\nprovider: jobstreet\napi: https://jobstreet.com.au/api/jobsearch/v5/search\n// after\nprovider: jobstreet\napi: https://www.seek.com.au/api/jobsearch/v5/search\nsiteKey: AUD-Main","handlingStrategy":"validation","validationCode":"const ALLOWED = ['id.jobstreet.com','www.jobstreet.com','www.jobstreet.co.id','jobstreet.com','jobstreet.co.id','sg.jobstreet.com','my.jobstreet.com','hk.jobsdb.com','www.seek.com.au','www.seek.co.nz'];\nif (!ALLOWED.includes(new URL(entry.api).hostname)) throw new Error('hostname not allowlisted for jobstreet');","typeGuard":"const isAllowedJobstreetHost = (s) => { try { return ALLOWED_JOBSTREET_HOSTS.has(new URL(s).hostname); } catch { return false; } };","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (e) {\n  if (e.message.includes('untrusted hostname')) {\n    console.error(`Fix ${entry.name}: use an allowlisted SEEK/Jobstreet host`);\n  }\n}","preventionTips":["Pick the host from the documented market table (ID-Main/SG-Main/MY-Main/HK-Main) rather than typing a URL by hand.","Remember Hong Kong is hk.jobsdb.com and Australia/NZ are seek.com.au / seek.co.nz, not jobstreet domains.","Never point api: at proxies or mirror hosts; they will always be rejected."],"tags":["ssrf","allowlist","url-validation","config"],"backgroundTag":"invalid-config-value","analyzedSha":"e7abd431fce9348a95261acac9e0c14779c35df8","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}