{"record":{"id":"ca065e2976f629b6","repo":"santifer/career-ops","slug":"4dayweek-untrusted-hostname-parsed-hostname","errorCode":null,"errorMessage":"4dayweek: untrusted hostname \"${parsed.hostname}\" — must be ${TRUSTED_HOST}","messagePattern":"4dayweek: untrusted hostname \"(.+?)\" — must be (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/4dayweek.mjs","lineNumber":57,"sourceCode":"      }\n    } catch {\n      // Ignore malformed URLs; another provider may still claim the entry.\n    }\n  }\n  return null;\n}\n\n/** @param {string} url */\nfunction assertFourDayUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`4dayweek: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`4dayweek: URL must use HTTPS: ${url}`);\n  if (parsed.hostname !== TRUSTED_HOST) {\n    throw new Error(`4dayweek: 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// NaN-safe: posted is epoch SECONDS → ms; anything non-finite yields undefined.\nfunction toEpochMs(seconds) {\n  return Number.isFinite(seconds) ? seconds * 1000 : undefined;\n}\n\n/**\n * Normalize a single 4 Day Week job. Exported for unit tests.","sourceCodeStart":39,"sourceCodeEnd":75,"githubUrl":"https://github.com/santifer/career-ops/blob/1696bec4d021768e7359f9aad6b329cba883da20/providers/4dayweek.mjs#L39-L75","documentation":"The final allowlist check in assertFourDayUrl(): the URL is valid HTTPS, but its hostname is not the trusted host ('4dayweek.io', providers/4dayweek.mjs:21). This is the SSRF defense — it guarantees the provider only ever requests the vendor's own domain, so a crafted entry cannot make the scanner fetch internal or arbitrary hosts.","triggerScenarios":"Passing https:// URLs with a different hostname — mirrors, CDN hosts like www.4dayweek.io if it's not the constant, lookalike domains, or attacker-supplied URLs from a scraped posting — into the 4dayweek provider's URL path.","commonSituations":"Config pointing at a mirror or proxy of 4dayweek.io; URLs harvested from a page that link to the actual employer ATS rather than 4dayweek.io; a typo'd or lookalike domain; environment-specific overrides swapping in a staging host.","solutions":["Use only URLs on https://4dayweek.io (the value of TRUSTED_HOST in providers/4dayweek.mjs:21).","Normalize URLs before passing: strip tracking params and use the canonical 4dayweek.io job link.","If you genuinely need another host, extend TRUSTED_HOST to an allowlist in the provider source — don't bypass the check at call sites.","If URLs come from scraped pages, extract the job slug and rebuild the URL on the trusted host."],"exampleFix":"// before\nassertFourDayUrl('https://www.4dayweek.io/job/9'); // hostname mismatch\n// after\nassertFourDayUrl('https://4dayweek.io/job/9');","handlingStrategy":"validation","validationCode":"const TRUSTED_HOST = '4dayweek.io';\nfunction isTrustedUrl(url) {\n  try {\n    const u = new URL(url);\n    return u.protocol === 'https:' && u.hostname === TRUSTED_HOST;\n  } catch { return false; }\n}\nif (!isTrustedUrl(entry.url)) throw new Error(`Refusing non-trusted URL: ${entry.url}`);","typeGuard":"function isTrusted4DayUrl(v) {\n  if (typeof v !== 'string') return false;\n  try {\n    const u = new URL(v);\n    return u.protocol === 'https:' && u.hostname === '4dayweek.io';\n  } catch { return false; }\n}","tryCatchPattern":"try {\n  provider.check(url);\n} catch (err) {\n  if (err.message.includes('untrusted hostname')) {\n    console.warn(`Blocked SSRF-suspect URL (${err.message}); rebuilding on trusted host from slug ${slug}`);\n    return provider.check(`https://4dayweek.io/job/${encodeURIComponent(slug)}`);\n  }\n  throw err;\n}","preventionTips":["Rebuild job URLs on the trusted host from slugs/IDs instead of trusting scraped hrefs.","Never point provider entries at mirrors, proxies, or www-variants of the trusted domain.","Keep SSRF allowlist checks intact — do not bypass them at call sites or in tests with mocks that skip validation.","Log-and-drop untrusted URLs during scraping rather than feeding them to the provider."],"tags":["ssrf","allowlist","url-validation","security"],"backgroundTag":"untrusted-hostname-blocked","analyzedSha":"1696bec4d021768e7359f9aad6b329cba883da20","analyzedAt":"2026-09-01T19:19:23.111Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}