{"record":{"id":"67106249177ac59b","repo":"santifer/career-ops","slug":"workable-untrusted-hostname-parsed-hostname","errorCode":null,"errorMessage":"workable: untrusted hostname \"${parsed.hostname}\" — must be one of: ${[...ALLOWED_WORKABLE_HOSTS].join(', ')}","messagePattern":"workable: untrusted hostname \"(.+?)\" — must be one of: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/workable.mjs","lineNumber":118,"sourceCode":"// same host, so this process never needs more than one in-flight request to\n// it at a time.\nlet workableQueue = Promise.resolve();\nfunction serialized(fn) {\n  const result = workableQueue.then(fn, fn);\n  workableQueue = result.then(() => undefined, () => undefined);\n  return result;\n}\n\nfunction assertWorkableUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`workable: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`workable: URL must use HTTPS: ${url}`);\n  if (!ALLOWED_WORKABLE_HOSTS.has(parsed.hostname)) {\n    throw new Error(`workable: untrusted hostname \"${parsed.hostname}\" — must be one of: ${[...ALLOWED_WORKABLE_HOSTS].join(', ')}`);\n  }\n  return url;\n}\n\n/**\n * Extract the account slug from a tracked_companies entry's careers_url.\n * @returns {string|null}\n */\nexport function resolveWorkableSlug(entry) {\n  const raw = entry && 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;","sourceCodeStart":100,"sourceCodeEnd":136,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/workable.mjs#L100-L136","documentation":"SSRF guard: the URL parsed and is https, but its hostname is not in ALLOWED_WORKABLE_HOSTS (apply.workable.com only). Built URLs always target apply.workable.com, so this fires only if a template host is changed OR the allowlist is emptied/mis-edited.","triggerScenarios":"A maintainer edits a URL template to a different host, or removes apply.workable.com from ALLOWED_WORKABLE_HOSTS.","commonSituations":"Refactor that accidentally clears the allowlist; a bad merge.","solutions":["Keep the templates on apply.workable.com and keep that host in ALLOWED_WORKABLE_HOSTS."],"exampleFix":"// before\nconst ALLOWED_WORKABLE_HOSTS = new Set([]);\n// after\nconst ALLOWED_WORKABLE_HOSTS = new Set(['apply.workable.com']);","handlingStrategy":"validation","validationCode":"for (const tpl of [widgetUrlFor, feedUrlFor]) {\n  const u = new URL(tpl(\"acme\"));\n  if (!ALLOWED_WORKABLE_HOSTS.has(u.hostname))\n    throw new Error(\"workable: template host not in allowlist\");\n}","typeGuard":"const isAllowedHost = (s, set) => { try { return set.has(new URL(s).hostname); } catch { return false; } };","tryCatchPattern":null,"preventionTips":["Keep ALLOWED_WORKABLE_HOSTS non-empty and the templates on apply.workable.com.","Add a CI test asserting both templates resolve to an allowed host."],"tags":["validation","url","ssrf-guard","hostname","workable","invariant"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}