{"record":{"id":"40a14a401389a61b","repo":"santifer/career-ops","slug":"jobbankca-untrusted-hostname-parsed-hostname","errorCode":null,"errorMessage":"jobbankca: untrusted hostname \"${parsed.hostname}\" — must be ${TRUSTED_HOST}","messagePattern":"jobbankca: untrusted hostname \"(.+?)\" — must be (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/jobbankca.mjs","lineNumber":102,"sourceCode":"}\n\n/** @param {string} keyword @param {number} page */\nexport function buildFeedUrl(keyword, page) {\n  const params = new URLSearchParams({ searchstring: keyword, locationstring: '', page: String(page) });\n  return `${FEED_URL}?${params.toString()}`;\n}\n\n/** @param {string} url */\nexport function assertJobBankUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`jobbankca: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`jobbankca: URL must use HTTPS: ${url}`);\n  if (parsed.hostname !== TRUSTED_HOST) {\n    throw new Error(`jobbankca: untrusted hostname \"${parsed.hostname}\" — must be ${TRUSTED_HOST}`);\n  }\n  return url;\n}\n\n// Resolve an Atom element's inner text: unwrap CDATA, else decode entities.\nfunction extractText(inner) {\n  const cdata = inner.match(/^\\s*<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>\\s*$/);\n  if (cdata) return cdata[1].trim();\n  return decodeEntities(inner).trim();\n}\n\nfunction tagText(block, tag) {\n  const m = block.match(new RegExp(`<${tag}\\\\b[^>]*>([\\\\s\\\\S]*?)</${tag}>`, 'i'));\n  return m ? extractText(m[1]) : '';\n}\n\n// <link rel=\"alternate\" type=\"text/html\" href=\"...\"/> — an attribute here,\n// not inner text (Atom), unlike an RSS <link>text</link>. An Atom entry may","sourceCodeStart":84,"sourceCodeEnd":120,"githubUrl":"https://github.com/santifer/career-ops/blob/1696bec4d021768e7359f9aad6b329cba883da20/providers/jobbankca.mjs#L84-L120","documentation":"assertJobBankUrl restricts requests to a single trusted host (TRUSTED_HOST, jobbank.canada.ca). Even a valid HTTPS URL pointing at another hostname is rejected with this error. This is an SSRF/supply-chain guard: the provider will never fetch from an arbitrary domain.","triggerScenarios":"Calling assertJobBankUrl with an https URL whose hostname is not the trusted host — e.g. a mirror domain, a typo like jobbank.canada.ca.evil.io, job-bank.canada.ca, or a test stub URL like https://localhost:3000/atom.xml.","commonSituations":"Using a proxy or mock server URL in dev, typo'd host in portals.yml, or intentionally pointing the provider at a local caching mirror expecting it to work.","solutions":["Point the URL back at the official trusted host (jobbank.canada.ca)","For local testing, intercept at the HTTP client level (e.g. nock/undici mock) instead of changing the URL hostname","If you genuinely need another domain, that belongs in a different provider module, not jobbankca","Check the hostname for subtle typos or appended paths that make the host differ (host with port or subdomain won't match)"],"exampleFix":"// before\nconst url = 'https://job-bank.canada.ca/atom.xml';\n// after\nconst url = 'https://jobbank.canada.ca/atom.xml';","handlingStrategy":"validation","validationCode":"const TRUSTED = 'jobbank.canada.ca';\nfunction isTrustedHost(u) { try { return new URL(u).hostname === TRUSTED; } catch { return false; } }\nif (!isTrustedHost(cfg.url)) throw new Error(`config: host not allowed for jobbankca: ${cfg.url}`);","typeGuard":"function isTrustedJobBankUrl(v) {\n  try { return new URL(v).hostname === 'jobbank.canada.ca'; } catch { return false; }\n}","tryCatchPattern":"try {\n  assertJobBankUrl(url);\n} catch (e) {\n  if (e.message.includes('untrusted hostname')) {\n    console.error(`jobbankca only fetches from jobbank.canada.ca; got: ${url}`);\n    return null; // skip entry\n  }\n  throw e;\n}","preventionTips":["Copy the official URL from jobbank.canada.ca rather than typing it","For local dev, mock the HTTP client (nock/undici interceptor) instead of swapping hostnames","Keep other domains in their own provider configs","Watch for exact-match semantics: subdomains, ports, and www. prefixes all fail"],"tags":["ssrf","allowlist","hostname","jobbankca"],"backgroundTag":"untrusted-hostname","analyzedSha":"1696bec4d021768e7359f9aad6b329cba883da20","analyzedAt":"2026-09-01T19:19:23.111Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}