{"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":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/jobstreet.mjs","lineNumber":60,"sourceCode":"  'www.seek.co.nz',\n]);\n\n// v5 API paths (the client-side JS on jobstreet uses these relative paths\n// resolved against the current origin). We keep the allowlist for SSRF\n// protection on the base URL, then build the v5 search path from it.\nconst V5_SEARCH_PATH = '/api/jobsearch/v5/search';\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":42,"sourceCodeEnd":78,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/jobstreet.mjs#L42-L78","documentation":"Thrown by assertJobstreetUrl when entry.api's hostname is not in ALLOWED_JOBSTREET_HOSTS (id.jobstreet.com, www.jobstreet.com, www.jobstreet.co.id, jobstreet.com, jobstreet.co.id, sg.jobstreet.com, my.jobstreet.com, www.seek.com.au, www.seek.co.nz). This is the core SSRF allowlist; it fires when a user points api: at a Jobstreet/SEEK regional host the provider does not yet allow.","triggerScenarios":"Setting api: to a regional host not in the allowlist — e.g. ph.jobstreet.com (Philippines), th.jobstreet.com (Thailand), hk.jobstreet.com (Hong Kong), or a SEEK host other than the two Australian/NZ ones; pointing api: at a wholly different (non-Jobstreet) host.","commonSituations":"Targeting a Jobstreet market the provider was not configured for (PH/TH/HK are common omissions); assuming any *.jobstreet.com subdomain is accepted (it is not — the set is explicit); a typo in the hostname.","solutions":["If the regional host is legitimate and trusted, add it to ALLOWED_JOBSTREET_HOSTS in providers/jobstreet.mjs (and add a regression test).","Otherwise switch api: to one of the allowed hosts, or omit api: to use DEFAULT_API (id.jobstreet.com).","Double-check the hostname spelling against the allowlist in the error message."],"exampleFix":"// before — regional host not in the allowlist\nconst ALLOWED_JOBSTREET_HOSTS = new Set(['id.jobstreet.com', /* ... */]);\n\n// after — add the Philippine market\nconst ALLOWED_JOBSTREET_HOSTS = new Set(['id.jobstreet.com', 'ph.jobstreet.com', /* ... */]);","handlingStrategy":"validation","validationCode":"import { readFileSync } from 'node:fs';\n// Hard-code the same allowlist the provider uses, for a pre-scan lint.\nconst ALLOWED = new Set(['id.jobstreet.com','www.jobstreet.com','www.jobstreet.co.id','jobstreet.com','jobstreet.co.id','sg.jobstreet.com','my.jobstreet.com','www.seek.com.au','www.seek.co.nz']);\nfunction assertJobstreetApiHost(apiUrl) {\n  const u = new URL(apiUrl);\n  if (!ALLOWED.has(u.hostname)) {\n    throw new Error(`jobstreet: ${u.hostname} not allowlisted — add it to ALLOWED_JOBSTREET_HOSTS or use a supported host`);\n  }\n}","typeGuard":"/** True for a URL whose host is in the Jobstreet/SEEK allowlist. */\nfunction isJobstreetHostAllowed(url) {\n  try { return ALLOWED.has(new URL(url).hostname); } catch { return false; }\n}","tryCatchPattern":"try {\n  return await jobstreetProvider.fetch(entry, ctx);\n} catch (err) {\n  if (/untrusted hostname/.test(err.message)) {\n    console.error(`config: ${entry.name} — ${err.message} (add the host to ALLOWED_JOBSTREET_HOSTS if legitimate)`);\n  } else {\n    throw err;\n  }\n}","preventionTips":["When targeting a new Jobstreet market (PH/TH/HK), add its host to ALLOWED_JOBSTREET_HOSTS with a regression test.","Remember the allowlist is explicit — *.jobstreet.com is NOT a blanket pass.","If unsure, omit api: and rely on DEFAULT_API."],"tags":["config","url-validation","allowlist","jobstreet","ssrf"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}