{"record":{"id":"4b05f01049371a13","repo":"santifer/career-ops","slug":"breezy-untrusted-hostname-parsed-hostname","errorCode":null,"errorMessage":"breezy: untrusted hostname \"${parsed.hostname}\" — must match <tenant>.breezy.hr","messagePattern":"breezy: untrusted hostname \"(.+?)\" — must match <tenant>\\.breezy\\.hr","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"providers/breezy.mjs","lineNumber":28,"sourceCode":"// Breezy boards expose every published position as a public JSON array at\n// `<tenant>.breezy.hr/json` — title, absolute url, location, and a published\n// date, all in the list payload at zero token cost (no per-job request, so the\n// scanner stays zero-token). Breezy's authenticated REST API (api.breezy.hr) is\n// intentionally NOT used; only the public board feed.\n\nconst BREEZY_HOST_RE = /^[a-z0-9][a-z0-9-]*\\.breezy\\.hr$/;\n\n/** @param {string} url */\nfunction assertBreezyUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`breezy: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`breezy: URL must use HTTPS: ${url}`);\n  if (!BREEZY_HOST_RE.test(parsed.hostname)) {\n    throw new Error(`breezy: untrusted hostname \"${parsed.hostname}\" — must match <tenant>.breezy.hr`);\n  }\n  return url;\n}\n\n/**\n * Resolve the tenant origin (`https://<tenant>.breezy.hr`) from an entry.\n * Honours an explicit `api:` URL, else parses `careers_url`.\n * @param {import('./_types.js').PortalEntry} entry\n * @returns {string | null}\n */\nfunction resolveOrigin(entry) {\n  const rawApi = typeof entry.api === 'string' ? entry.api : '';\n  const rawCareers = typeof entry.careers_url === 'string' ? entry.careers_url : '';\n  const raw = (rawApi || rawCareers).trim();\n  if (!raw) return null;\n  let parsed;\n  try {\n    parsed = new URL(raw);","sourceCodeStart":10,"sourceCodeEnd":46,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/breezy.mjs#L10-L46","documentation":"assertBreezyUrl throws this when the URL is valid https but its hostname does not match the pattern /^[a-z0-9][a-z0-9-]*\\.breezy\\.hr$/. Because Breezy tenants live on per-customer subdomains, the SSRF guard uses a regex allowlist of <tenant>.breezy.hr shapes instead of a static list; a host outside that shape could be an arbitrary fetch target and is rejected.","triggerScenarios":"A portals entry pointing at 'https://acme.breezy.com', 'https://breezy.hr' (no tenant), 'https://acme.vendor.breezy.hr' (multi-level subdomain), uppercase 'https://Acme.breezy.hr', or any unrelated host passed as api:/careers_url into the breezy provider's fetch().","commonSituations":"Company moved from Breezy to another ATS but the config entry kept the old URL shape; copy-pasting the company's main website instead of the board URL; wrong provider assignment — an entry for Greenhouse/Lever routed to the breezy provider; typo in the tenant subdomain.","solutions":["Set the entry URL to the real tenant board: 'https://<tenant>.breezy.hr' (single lowercase subdomain, exactly one dot before breezy.hr).","Verify the tenant subdomain by loading the board in a browser — it is what appears before .breezy.hr.","Lowercase the hostname; the regex rejects uppercase letters.","If the company is not on Breezy HR at all, remove the entry from the breezy provider's scope or fix the provider detection in portals.yml."],"exampleFix":"# before\ncareers_url: https://acme.breezy.com\n# after\ncareers_url: https://acme.breezy.hr","handlingStrategy":"validation","validationCode":"const BREEZY_HOST_RE = /^[a-z0-9][a-z0-9-]*\\.breezy\\.hr$/;\nfunction isTrustedBreezyHost(url) {\n  try { return BREEZY_HOST_RE.test(new URL(url).hostname); } catch { return false; }\n}\nif (!isTrustedBreezyHost(entry.careers_url)) throw new Error(`config: not a breezy.hr board: ${entry.careers_url}`);","typeGuard":"function isBreezyHost(hostname) { return /^[a-z0-9][a-z0-9-]*\\.breezy\\.hr$/.test(hostname); }","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).startsWith('breezy: untrusted hostname')) {\n    console.warn(`Skipping ${entry.name}: not a <tenant>.breezy.hr board — check portals.yml`);\n    return null;\n  }\n  throw err;\n}","preventionTips":["Confirm the company actually hosts its board on breezy.hr before adding the entry.","Use the tenant subdomain exactly as it appears in the browser address bar, lowercased.","Run provider.detect() on every entry at startup to catch host-shape mismatches before fetching.","Never bypass the hostname guard with a proxy or hosts-file trick."],"tags":["url","ssrf","allowlist","security","config"],"backgroundTag":"untrusted-hostname","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"}