{"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":"exception","errorClass":"Error","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/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/breezy.mjs#L10-L46","documentation":"The host-allowlist step of Breezy's SSRF guard. The hostname must match `/^[a-z0-9][a-z0-9-]*\\.breezy\\.hr$/` — a single lowercase tenant label (alphanumeric + hyphens, not starting with a hyphen) followed by `.breezy.hr`. Uppercase, multi-label tenants, or other domains are rejected.","triggerScenarios":"`BREEZY_HOST_RE.test(parsed.hostname)` is false: hostname is uppercase (`Acme.breezy.hr`), has a multi-part tenant (`jobs.acme.breezy.hr`), uses a different domain, or is the apex (`breezy.hr`).","commonSituations":"An uppercase tenant name, a deep subdomain, a domain typo, or a URL pointing at a non-Breezy host.","solutions":["Use a single lowercase tenant label: `https://acme.breezy.hr`.","If a multi-label tenant is legitimate, lower-case the host and widen the regex only if you trust the labels.","Confirm the host is actually a Breezy tenant before keeping the provider."],"exampleFix":"# before (rejected — uppercase + path-style host)\n- name: Acme\n  api: https://Jobs.Acme.breezy.hr\n\n# after\n- name: Acme\n  api: https://acme.breezy.hr","handlingStrategy":"validation","validationCode":"const RE = /^[a-z0-9][a-z0-9-]*\\.breezy\\.hr$/;\nfunction assertBreezyHost(u) {\n  const h = new URL(u).hostname;\n  if (!RE.test(h)) throw new Error(`breezy: host ${h} must match <tenant>.breezy.hr (lowercase, single label)`);\n}\nif (entry.api) assertBreezyHost(entry.api);\nif (entry.careers_url) assertBreezyHost(entry.careers_url);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Lowercase tenant names in config before validation.","Reject multi-label or uppercase hosts at config lint time.","Confirm the host is a real Breezy tenant before assigning the provider."],"tags":["breezy","ssrf","host-allowlist","security","regex"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}