{"record":{"id":"2881fbc8e24e2687","repo":"santifer/career-ops","slug":"comeet-untrusted-hostname-parsed-hostname","errorCode":null,"errorMessage":"comeet: untrusted hostname \"${parsed.hostname}\" — must be ${COMEET_API_HOST}","messagePattern":"comeet: untrusted hostname \"(.+?)\" — must be (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"providers/comeet.mjs","lineNumber":38,"sourceCode":"  try {\n    parsed = new URL(raw);\n  } catch {\n    return false;\n  }\n  return parsed.protocol === 'https:' && parsed.hostname === COMEET_API_HOST && parsed.pathname.startsWith('/careers-api/');\n}\n\n/** @param {string} url */\nfunction assertComeetUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`comeet: invalid URL: ${redactToken(url)}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`comeet: URL must use HTTPS: ${redactToken(url)}`);\n  if (parsed.hostname !== COMEET_API_HOST)\n    throw new Error(`comeet: untrusted hostname \"${parsed.hostname}\" — must be ${COMEET_API_HOST}`);\n  if (!parsed.pathname.startsWith('/careers-api/'))\n    throw new Error(`comeet: URL path must be the careers-api endpoint: ${redactToken(url)}`);\n  return url;\n}\n\n// Redact the per-tenant ?token= so neither the (informational, possibly-logged)\n// DetectHit url nor a thrown validation error carries the secret. Best-effort:\n// falls back to a regex strip when the value can't be parsed as a URL.\nfunction redactToken(url) {\n  try {\n    const parsed = new URL(url);\n    if (parsed.searchParams.has('token')) parsed.searchParams.set('token', 'REDACTED');\n    return parsed.href;\n  } catch {\n    return typeof url === 'string' ? url.replace(/([?&]token=)[^&#]*/gi, '$1REDACTED') : url;\n  }\n}\n","sourceCodeStart":20,"sourceCodeEnd":56,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/comeet.mjs#L20-L56","documentation":"As an SSRF defense, the Comeet provider pins the API hostname to the single fixed origin www.comeet.co (combined with redirect:'error' so a redirect can't move the request off-host). A URL that parses and is https but points at any other hostname is rejected with this error naming the offending host. This prevents a crafted portal entry from making the scanner call an arbitrary internal service.","triggerScenarios":"fetch() gets an entry whose api/careers_url is https and URL-parseable but whose hostname is not exactly 'www.comeet.co' — e.g. comeet.com (no www), a spoofed subdomain (www.comeet.co.evil.io), a company's own careers domain, or an IP address.","commonSituations":"Using the tenant's branded careers URL (acme.comeet.co or acme.com/jobs) instead of the fixed www.comeet.co careers-api origin; a typo like ww.comeet.co; hostile/misconfigured portal entries in a shared config; assuming Comeet works like Greenhouse where per-tenant subdomains (boards.greenhouse.io/<slug>) are the norm.","solutions":["Rewrite the entry's api to the canonical origin: https://www.comeet.co/careers-api/2.0/company/<uid>/positions?token=<token>","Find the company-uid and token from the tenant's actual careers-api link (inspect the network tab on their careers page) — Comeet has no slug→API shortcut","Check for lookalike/typo hostnames (www.comeet.com, comeet.co without www) and correct them","If you legitimately need a different host (self-hosted proxy), you must modify COMEET_API_HOST in providers/comeet.mjs — don't try to sneak it via the entry config"],"exampleFix":"// before\napi: https://acme.comeet.co/careers-api/2.0/company/acme/positions?token=abc\n// after\napi: https://www.comeet.co/careers-api/2.0/company/acme/positions?token=abc","handlingStrategy":"validation","validationCode":"const COMEET_API_HOST = 'www.comeet.co';\nfunction isCanonicalComeetOrigin(raw) {\n  try {\n    const u = new URL(raw);\n    return u.protocol === 'https:' && u.hostname === COMEET_API_HOST && u.pathname.startsWith('/careers-api/');\n  } catch { return false; }\n}\nif (!isCanonicalComeetOrigin(entry.api)) throw new Error(`entry ${entry.name}: comeet api must be https://www.comeet.co/careers-api/...`);","typeGuard":"function isComeetApiUrl(raw) {\n  if (typeof raw !== 'string' || !raw) return false;\n  try {\n    const u = new URL(raw);\n    return u.protocol === 'https:' && u.hostname === 'www.comeet.co' && u.pathname.startsWith('/careers-api/');\n  } catch { return false; }\n}","tryCatchPattern":"try {\n  assertComeetUrl(entry.api);\n} catch (err) {\n  if (String(err.message).includes('untrusted hostname')) {\n    logger.error({entry: entry.name}, 'comeet api must point at https://www.comeet.co — rebuild the URL with the tenant uid and token');\n  } else throw err;\n}","preventionTips":["Remember Comeet has no slug shortcut: always supply the full careers-api URL with company uid + token from the tenant's live careers page","Never use branded tenant domains (acme.comeet.co) — only the fixed www.comeet.co origin is allowed","Beware lookalike hostnames (www.comeet.co.evil.io); the exact-hostname check is intentional SSRF protection","Keep redirect:'error' semantics in mind: the pinned host must be the first hop, not just the final one","Review any portal entry whose URL you didn't construct yourself before adding it to shared config"],"tags":["security","ssrf","url","config"],"backgroundTag":"invalid-url-format","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"}