{"record":{"id":"8348050da63b91e4","repo":"santifer/career-ops","slug":"personio-url-must-use-https-url","errorCode":null,"errorMessage":"personio: URL must use HTTPS: ${url}","messagePattern":"personio: URL must use HTTPS: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/personio.mjs","lineNumber":23,"sourceCode":"// `https://<slug>.jobs.personio.de/xml` (common across DACH/EU companies).\n// Auto-detects from a `<slug>.jobs.personio.(de|com)` careers host like\n// workable/recruitee. Per-tenant subdomains are the variable part, so the\n// SSRF defence is an anchored host regex rather than a static allowlist.\n//\n// The feed is a flat, well-defined XML document, so it is parsed in-process\n// with a tiny tag extractor (no new dependency — the repo ships none for XML).\n\nconst PERSONIO_HOST_RE = /^[a-z0-9][a-z0-9-]*\\.jobs\\.personio\\.(de|com)$/;\n\n/** @param {string} url */\nfunction assertPersonioUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`personio: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`personio: URL must use HTTPS: ${url}`);\n  if (!PERSONIO_HOST_RE.test(parsed.hostname))\n    throw new Error(`personio: untrusted hostname \"${parsed.hostname}\" — must match <slug>.jobs.personio.(de|com)`);\n  return url;\n}\n\n/**\n * Resolve the tenant host (e.g. `acme.jobs.personio.de`) from a careers_url.\n * Returns null for non-Personio or malformed URLs.\n * @param {import('./_types.js').PortalEntry} entry\n */\nfunction resolveHost(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;","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/personio.mjs#L5-L41","documentation":"Thrown by personio's assertPersonioUrl() when the URL parses but protocol is not 'https:'. Second SSRF gate preventing plaintext HTTP fetches to Personio career sites. Combined with redirect:'error' in fetch(), this ensures the entire request chain stays encrypted and on-domain.","triggerScenarios":"A valid URL with http: scheme: entry.careers_url prefixed http://, or a test against http://localhost. Since fetch() constructs `https://${host}/xml` from resolveHost, the protocol is normally https by construction — this fires only if assertPersonioUrl is called with a directly-supplied http URL.","commonSituations":"Portals.yml authored with http://. A config tool defaulting to http. Local testing against a non-TLS server.","solutions":["Set careers_url to https:// in portals.yml.","Use HTTPS for local mock servers in tests.","Verify no intermediary downgrades the scheme."],"exampleFix":"// before\ncareers_url: 'http://acme.jobs.personio.de'\n\n// after\ncareers_url: 'https://acme.jobs.personio.de'","handlingStrategy":"validation","validationCode":"/** Normalize Personio URL to HTTPS. */\nfunction ensureHttps(url) {\n  if (typeof url !== 'string') return null;\n  return url.replace(/^http:\\/\\//i, 'https://');\n}\n\nentry.careers_url = ensureHttps(entry.careers_url) || entry.careers_url;","typeGuard":"/** @param {string} url @returns {boolean} */\nfunction isHttpsUrl(url) {\n  try { return new URL(url).protocol === 'https:'; } catch { return false; }\n}","tryCatchPattern":"try {\n  await personioProvider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).includes('must use HTTPS')) {\n    entry.careers_url = (entry.careers_url || '').replace(/^http:/i, 'https:');\n    await personioProvider.fetch(entry, ctx);\n  } else throw err;\n}","preventionTips":["Author Personio URLs with https:// in config.","Lint config for http:// entries.","Use HTTPS mock servers in tests."],"tags":["url-validation","ssrf-guard","https","personio","config"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}