{"record":{"id":"9bfd7f3f71ef3a3f","repo":"santifer/career-ops","slug":"personio-invalid-url-url","errorCode":null,"errorMessage":"personio: invalid URL: ${url}","messagePattern":"personio: invalid URL: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/personio.mjs","lineNumber":21,"sourceCode":"\n// Personio provider — hits the public, no-auth XML jobs feed at\n// `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);","sourceCodeStart":3,"sourceCodeEnd":39,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/personio.mjs#L3-L39","documentation":"Thrown by personio's assertPersonioUrl() when new URL(url) throws — the URL is syntactically unparseable. First of three SSRF gates (valid URL → HTTPS → trusted hostname regex) for Personio job boards. Personio career sites use the pattern <slug>.jobs.personio.(de|com), and the hostname regex enforces this tenant-subdomain structure.","triggerScenarios":"Called with an unparseable URL: undefined, empty string, spaces, or a schemeless path like 'acme.jobs.personio.de/xml'. The guard is called from fetch() after constructing the feed URL from resolveHost(). Since fetch() builds the URL as `https://${host}/xml`, the host itself would need to be malformed for this to fire — more likely from a direct/test invocation of assertPersonioUrl with bad input.","commonSituations":"A portals.yml personio entry has careers_url missing or malformed. The entry was built programmatically without the URL field. resolveHost() somehow returned a value containing illegal hostname characters. Testing with a fixture path.","solutions":["Log the url passed to assertPersonioUrl to identify the malformed string.","Ensure the portals.yml personio entry has careers_url set to https://<slug>.jobs.personio.de or .com.","If resolveHost returned a bad host, check entry.careers_url — resolveHost extracts the hostname from it."],"exampleFix":"// before\ncareers_url: 'acme.jobs.personio.de'  // missing https://\n\n// after\ncareers_url: 'https://acme.jobs.personio.de'","handlingStrategy":"validation","validationCode":"/** Validate URL string is parseable before passing to assertPersonioUrl. */\nfunction isValidUrlString(url) {\n  return typeof url === 'string'\n    && url.length > 0\n    && (() => { try { new URL(url); return true; } catch { return false; } })();\n}\n\nif (!isValidUrlString(entry.careers_url)) {\n  console.warn(`personio entry ${entry.name} has invalid URL`);\n  continue;\n}","typeGuard":"/** @param {unknown} url @returns {url is string} */\nfunction isParseableUrl(url) {\n  if (typeof url !== 'string' || !url) return false;\n  try { new URL(url); return true; } catch { return false; }\n}","tryCatchPattern":"try {\n  await personioProvider.fetch(entry, ctx);\n} catch (err) {\n  if (String(err.message).startsWith('personio: invalid URL')) {\n    console.warn(`skipping personio entry ${entry.name}: malformed URL`);\n    continue;\n  }\n  throw err;\n}","preventionTips":["Validate URL fields in portals.yml at config-load time.","Ensure Personio URLs include https:// and follow the <slug>.jobs.personio.(de|com) pattern.","Call detect(entry) and skip null results before fetch()."],"tags":["url-validation","ssrf-guard","personio","config"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}