{"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":24,"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 */\nconst PERSONIO_SLUG_RE = /^[a-z0-9][a-z0-9-]{0,62}$/i;\n\nfunction resolveHost(entry) {\n  // An explicit `personio: <slug>` pins the tenant directly. Needed because many\n  // companies embed the Personio tenant as an iframe on a branded careers page,\n  // so careers_url points at the company domain while the feed lives at\n  // <slug>.jobs.personio.de. The slug is charset-restricted here and the\n  // resulting URL still goes through assertPersonioUrl(), so the host allowlist","sourceCodeStart":6,"sourceCodeEnd":42,"githubUrl":"https://github.com/santifer/career-ops/blob/e7abd431fce9348a95261acac9e0c14779c35df8/providers/personio.mjs#L6-L42","documentation":"assertPersonioUrl requires every URL it accepts to use HTTPS; a parsed URL with a different protocol (http:, ftp:) is rejected. Personio feeds contain candidate-facing job data and the provider enforces TLS both for security and to keep the SSRF guard simple — the same allowlisted host must be reached over https only.","triggerScenarios":"Calling any code path through assertPersonioUrl (personio.mjs line 24) with a careers_url whose scheme is http:// — most commonly http://acme.jobs.personio.de/xml — or whose scheme came out as something else due to a malformed string.","commonSituations":"Config authored by hand with http:// out of habit; an old bookmark or internal link pre-dating an HTTPS migration; documentation snippets copied with http; or a proxy setup that rewrites https to http internally.","solutions":["Change the scheme to https:// in the portals.yml careers_url value.","Confirm the tenant feed actually serves https (it always does for *.jobs.personio.de/com) by opening the URL in a browser.","If the URL comes from a script that builds feed URLs, fix the template there (e.g. `https://${host}/xml`).","Search the repo for `http://` occurrences in portals.yml to catch other insecure entries at once."],"exampleFix":"// before (portals.yml)\ncareers_url: http://acme.jobs.personio.de/xml\n// after\ncareers_url: https://acme.jobs.personio.de/xml","handlingStrategy":"validation","validationCode":"function isHttpsUrl(u) {\n  if (typeof u !== 'string') return false;\n  try { return new URL(u).protocol === 'https:'; } catch { return false; }\n}\nif (!isHttpsUrl(entry.careers_url)) throw new Error(`personio: careers_url for ${entry.name} must use https://`);","typeGuard":"function isHttpsPersonioUrl(u) {\n  if (typeof u !== 'string') return false;\n  try {\n    const parsed = new URL(u);\n    return parsed.protocol === 'https:' &&\n      /^[a-z0-9][a-z0-9-]*\\.jobs\\.personio\\.(de|com)$/.test(parsed.hostname);\n  } catch { return false; }\n}","tryCatchPattern":"try {\n  await personioProvider.fetch(entry, ctx);\n} catch (e) {\n  if (String(e.message).startsWith('personio: URL must use HTTPS')) {\n    logger.warn({ entry: entry.name, url: entry.careers_url }, 'insecure scheme — rewrite http:// to https:// in portals.yml');\n    return null;\n  }\n  throw e;\n}","preventionTips":["Default to https:// when authoring any careers_url; never author http://.","Lint portals.yml in CI: reject any careers_url whose parsed protocol is not https:.","If a migration/old doc contains http links, bulk-replace schemes before committing config.","Remember the provider appends /xml to an https base — keep the base https."],"tags":["https","url-validation","config","personio"],"backgroundTag":"invalid-url","analyzedSha":"e7abd431fce9348a95261acac9e0c14779c35df8","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}