{"record":{"id":"7f27540777bf4ef6","repo":"santifer/career-ops","slug":"arbeitnow-untrusted-hostname-parsed-hostname","errorCode":null,"errorMessage":"arbeitnow: untrusted hostname \"${parsed.hostname}\" — must be ${TRUSTED_HOST}","messagePattern":"arbeitnow: untrusted hostname \"(.+?)\" — must be (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/arbeitnow.mjs","lineNumber":34,"sourceCode":"// Wire in via a `job_boards:` entry with `provider: arbeitnow`.\n\nconst FEED_BASE = 'https://www.arbeitnow.com/api/job-board-api';\nconst TRUSTED_HOST = 'www.arbeitnow.com';\nconst PER_PAGE = 100;\nconst DEFAULT_MAX_PAGES = 3;\nconst MAX_PAGES_CAP = 50;\n\n/** @param {string} url */\nfunction assertArbeitnowUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`arbeitnow: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`arbeitnow: URL must use HTTPS: ${url}`);\n  if (parsed.hostname !== TRUSTED_HOST) {\n    throw new Error(`arbeitnow: untrusted hostname \"${parsed.hostname}\" — must be ${TRUSTED_HOST}`);\n  }\n  return url;\n}\n\n/** Resolve the page cap: a positive integer `max_pages` on the entry, capped. */\nfunction resolveMaxPages(entry) {\n  const v = entry?.max_pages;\n  if (Number.isInteger(v) && v > 0) return Math.min(v, MAX_PAGES_CAP);\n  return DEFAULT_MAX_PAGES;\n}\n\n/**\n * Normalize a single Arbeitnow job. Exported for unit tests.\n *\n * Field mapping → the normalized Job shape:\n *   - title:    `title`, trimmed (items without one are dropped).\n *   - url:      `url` — an absolute `https:` posting URL host-locked to\n *               www.arbeitnow.com (an off-host or non-https URL is untrusted and","sourceCodeStart":16,"sourceCodeEnd":52,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/arbeitnow.mjs#L16-L52","documentation":"The hostname-allowlist step of arbeitnow's SSRF guard. After scheme validation, the provider requires `parsed.hostname === 'www.arbeitnow.com'` exactly. Any other host — even a look-alike — is rejected so a server-side or config-level redirect cannot exfiltrate requests to an attacker-controlled domain.","triggerScenarios":"`parsed.hostname` is anything other than `www.arbeitnow.com`: a subdomain (`api.arbeitnow.com`), an apex (`arbeitnow.com`), a spoofed host (`www.arbeitnow.com.evil.tld`), or an entirely different domain.","commonSituations":"Pointing the provider at a mirror, staging, or proxy host; a typo in `FEED_BASE`; or an attempt to reuse the provider for a different job board that happens to share the API shape.","solutions":["Keep `TRUSTED_HOST` as `www.arbeitnow.com` and use only the canonical API endpoint.","If a genuinely new host must be allowed, add it explicitly to a trusted set rather than loosening the check to a suffix.","Audit the URL for trailing-host tricks (e.g. `www.arbeitnow.com.attacker.tld`) that pass naive `endsWith` checks."],"exampleFix":"// before (rejected — wrong host)\nconst FEED_BASE = 'https://api.arbeitnow.com/api/job-board-api';\n\n// after\nconst FEED_BASE = 'https://www.arbeitnow.com/api/job-board-api';","handlingStrategy":"validation","validationCode":"const TRUSTED_HOST = 'www.arbeitnow.com';\nfunction assertTrustedHost(u) {\n  const p = new URL(u);\n  if (p.hostname !== TRUSTED_HOST) {\n    throw new Error(`untrusted host ${p.hostname}; expected ${TRUSTED_HOST}`);\n  }\n}\nassertTrustedHost(FEED_BASE);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never loosen the host check to a suffix/contains test — that admits spoofed hosts.","If a new host must be trusted, add it to an explicit allowlist set and document why.","Audit config for URLs that bypass the constant FEED_BASE."],"tags":["arbeitnow","ssrf","host-allowlist","security"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}