{"record":{"id":"ba0bb1cf4350a99d","repo":"santifer/career-ops","slug":"recruitee-url-must-use-https-url","errorCode":null,"errorMessage":"recruitee: URL must use HTTPS: ${url}","messagePattern":"recruitee: URL must use HTTPS: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/recruitee.mjs","lineNumber":21,"sourceCode":"\n// Recruitee provider — hits the public per-tenant offers API.\n// Auto-detects from careers_url pattern `https://<slug>.recruitee.com`.\n// Per-tenant subdomains are the variable part — SSRF defence uses a\n// regex match on `<safe-slug>.recruitee.com` rather than a static\n// allowlist.\n\nimport { htmlToText } from './_html-to-text.mjs';\n\nconst RECRUITEE_HOST_RE = /^[a-z0-9][a-z0-9-]*\\.recruitee\\.com$/;\n\nfunction assertRecruiteeUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`recruitee: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`recruitee: URL must use HTTPS: ${url}`);\n  if (!RECRUITEE_HOST_RE.test(parsed.hostname)) {\n    throw new Error(`recruitee: untrusted hostname \"${parsed.hostname}\" — must match <slug>.recruitee.com`);\n  }\n  return url;\n}\n\nfunction resolveApiUrl(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 (!RECRUITEE_HOST_RE.test(parsed.hostname)) return null;\n  return `https://${parsed.hostname}/api/offers/`;","sourceCodeStart":3,"sourceCodeEnd":39,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/recruitee.mjs#L3-L39","documentation":"URL scheme guard in the Recruitee provider (assertRecruiteeUrl): the parsed per-tenant careers URL parsed fine but its protocol is not https:. Part of the SSRF defence stack that also checks the host against the <safe-slug>.recruitee.com pattern; the scheme check simply comes later. The input at fault is the careers_url for the tenant.","triggerScenarios":"A careers_url like http://acme.recruitee.com or any non-https scheme (ftp:, file:) reaches assertRecruiteeUrl via detect()/fetch().","commonSituations":"A manually edited portals.yml entry uses http:// because the careers page redirected; an old config predating an HTTPS migration; tooling that lowercases or strips the scheme.","solutions":["Change the scheme to https:// in the entry","Verify Recruitee serves the tenant over HTTPS (it always does for *.recruitee.com)","Re-run the scan after fixing the URL"],"exampleFix":"// before\ncareers_url: http://acme.recruitee.com\n// after\ncareers_url: https://acme.recruitee.com","handlingStrategy":"validation","validationCode":"function isHttpsUrl(s) {\n  try { return new URL(s).protocol === 'https:'; } catch { return false; }\n}\nif (!isHttpsUrl(entry.careers_url)) throw new Error(`Use https:// for ${entry.name}`);","typeGuard":"function isSecureUrl(v) {\n  try { return v instanceof URL ? v.protocol === 'https:' : new URL(String(v)).protocol === 'https:'; } catch { return false; }\n}","tryCatchPattern":"try {\n  assertRecruiteeUrl(entry.careers_url);\n} catch (err) {\n  if (err.message.startsWith('recruitee: URL must use HTTPS')) {\n    const fixed = entry.careers_url.replace(/^http:/, 'https:');\n    console.warn(`Upgraded ${entry.name} careers_url to ${fixed}`);\n  }\n  throw err;\n}","preventionTips":["Never enter http:// URLs for ATS boards; Recruitee tenants are always HTTPS","Add a config check that rejects non-https careers_url at edit time","Treat http→https upgrades as safe — but re-verify the board after the change"],"tags":["security","url-validation","https"],"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"}