{"record":{"id":"d1ff1deddf8eef1e","repo":"santifer/career-ops","slug":"remotli-untrusted-hostname-parsed-hostname","errorCode":null,"errorMessage":"remotli: untrusted hostname \"${parsed.hostname}\" — must be remotli.ch","messagePattern":"remotli: untrusted hostname \"(.+?)\" — must be remotli\\.ch","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/remotli.mjs","lineNumber":243,"sourceCode":"  if (postedAt !== undefined) out.postedAt = postedAt;\n\n  const salary = resolveSalary(job);\n  if (salary) out.salary = salary;\n\n  return out;\n}\n\n/** Guard the API URL: HTTPS + remotli.ch only. */\nfunction assertRemotliUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`remotli: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`remotli: URL must use HTTPS: ${url}`);\n  if (!HOST_RE.test(parsed.hostname))\n    throw new Error(`remotli: untrusted hostname \"${parsed.hostname}\" — must be remotli.ch`);\n  return url;\n}\n\n/** @type {Provider} */\nexport default {\n  id: 'remotli',\n\n  detect(entry) {\n    const raw = typeof entry.careers_url === 'string' ? entry.careers_url : '';\n    if (!raw) return null;\n    let parsed;\n    try {\n      parsed = new URL(raw);\n    } catch {\n      return null;\n    }\n    if (parsed.protocol !== 'https:') return null;\n    if (!HOST_RE.test(parsed.hostname)) return null;","sourceCodeStart":225,"sourceCodeEnd":261,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/remotli.mjs#L225-L261","documentation":"remotli provider pins all requests to the remotli.ch host. assertRemotliUrl parses the URL and rejects any hostname not matching HOST_RE with this error. This prevents SSRF and misdirected requests by ensuring every request goes only to the trusted board host.","triggerScenarios":"Calling the remotli provider with a URL whose parsed hostname fails HOST_RE — e.g. https://api.example.com/jobs, a typo'd domain (remotli.com), a localhost/mirror host, or an attacker-controlled URL passed through an entry's config.","commonSituations":"Config pointing at a proxy or local mirror, a renamed/moved board endpoint, copy-pasted URLs from another provider, or a config-injection attempt supplying a foreign hostname.","solutions":["Use a URL whose hostname is exactly remotli.ch (check for typos like remotli.com or subdomain changes).","If you must route through a proxy, proxy at the network level — do not change the URL hostname.","Inspect the provider entry/portals.yml value that supplied the URL and correct it."],"exampleFix":"// before\nurl = 'https://mirror.example.com/remotli/jobs';\n// after\nurl = 'https://remotli.ch/api/jobs';","handlingStrategy":"validation","validationCode":"function isTrustedHost(url) { try { return new URL(url).hostname === 'remotli.ch'; } catch { return false; } }\nif (!isTrustedHost(entry.url)) throw new Error(`skip: untrusted host in ${entry.url}`);","typeGuard":"const isRemotliUrl = (u) => { try { return new URL(u).hostname === 'remotli.ch'; } catch { return false; } };","tryCatchPattern":"try { await provider.fetch(entry, ctx); } catch (e) { if (e.message.includes('untrusted hostname')) { console.error(`Entry ${entry.name} points at a non-remotli.ch host`); return null; } throw e; }","preventionTips":["Keep board hostnames in one config constant and compare before calls.","Beware lookalike domains (.com vs .ch) when editing config.","Never accept entry URLs from untrusted input without hostname allowlisting."],"tags":["url-validation","ssrf","security","hostname"],"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"}