{"record":{"id":"280485972fd101e4","repo":"santifer/career-ops","slug":"pinpoint-untrusted-hostname-parsed-hostname","errorCode":null,"errorMessage":"pinpoint: untrusted hostname \"${parsed.hostname}\" — must match <slug>.pinpointhq.com","messagePattern":"pinpoint: untrusted hostname \"(.+?)\" — must match <slug>\\.pinpointhq\\.com","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/pinpoint.mjs","lineNumber":34,"sourceCode":"\n// The tenant label must be a valid DNS label: it may contain hyphens but must\n// not start or end with one (so `acme-.pinpointhq.com` is rejected). The\n// optional trailing group keeps single-character labels (e.g. `a.pinpointhq.com`)\n// valid. detect() and fetch() both route through this constant via\n// resolveApiUrl()/assertPinpointUrl(), so the stricter check applies everywhere.\nconst PINPOINT_HOST_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.pinpointhq\\.com$/;\n\n/** @param {string} url */\nfunction assertPinpointUrl(url) {\n  let parsed;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(`pinpoint: invalid URL: ${url}`);\n  }\n  if (parsed.protocol !== 'https:') throw new Error(`pinpoint: URL must use HTTPS: ${url}`);\n  if (!PINPOINT_HOST_RE.test(parsed.hostname)) {\n    throw new Error(`pinpoint: untrusted hostname \"${parsed.hostname}\" — must match <slug>.pinpointhq.com`);\n  }\n  return url;\n}\n\nfunction resolveApiUrl(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;\n  }\n  if (parsed.protocol !== 'https:') return null;\n  if (!PINPOINT_HOST_RE.test(parsed.hostname)) return null;\n  return `https://${parsed.hostname}/postings.json`;\n}\n","sourceCodeStart":16,"sourceCodeEnd":52,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/pinpoint.mjs#L16-L52","documentation":"assertPinpointUrl only accepts hostnames matching PINPOINT_HOST_RE — <slug>.pinpointhq.com. The URL parsed and used HTTPS, but the hostname is outside that allowlist, so the provider refuses to send the request. This is the SSRF/trust guard: the hostname could be an attacker-controlled or simply wrong domain.","triggerScenarios":"Calling fetch or validation paths reaching assertPinpointUrl (pinpoint.mjs line 34) with a careers_url host like acme.pinpointhq.co (wrong TLD), jobs.acme.com (vanity domain), pinpointhq.com.evil.test, or an extra subdomain level.","commonSituations":"Company uses a vanity domain fronting Pinpoint instead of the tenant subdomain; TLD typo (.co vs .com); an entry actually belonging to a different ATS; malicious/mistaken edit of portals.yml pointing off-domain.","solutions":["Replace careers_url in portals.yml with the tenant's real https://<slug>.pinpointhq.com host (find the slug from the company's careers page or Pinpoint feed link).","If Pinpoint serves this tenant on a new host shape, update PINPOINT_HOST_RE at pinpoint.mjs line 22 and align the error message.","Resolve the vanity domain once (follow the redirect manually) and hardcode the underlying pinpointhq.com tenant host.","Do not bypass by enabling redirects in fetchText — the redirect:'error' flag plus this assertion is the SSRF guarantee."],"exampleFix":"// before (portals.yml)\ncareers_url: https://jobs.acme.com\n// after\ncareers_url: https://acme.pinpointhq.com","handlingStrategy":"validation","validationCode":"const PINPOINT_HOST_RE = /^[a-z0-9-]+\\.pinpointhq\\.com$/;\nexport function isPinpointUrl(u) {\n  try {\n    const parsed = new URL(u);\n    return parsed.protocol === 'https:' && PINPOINT_HOST_RE.test(parsed.hostname);\n  } catch { return false; }\n}\nif (!isPinpointUrl(entry.careers_url)) throw new Error(`pinpoint: careers_url for ${entry.name} not on Pinpoint allowlist`);","typeGuard":"function isPinpointTenantUrl(u) {\n  if (typeof u !== 'string') return false;\n  try {\n    const parsed = new URL(u);\n    return parsed.protocol === 'https:' && /^[a-z0-9-]+\\.pinpointhq\\.com$/.test(parsed.hostname);\n  } catch { return false; }\n}","tryCatchPattern":"try {\n  await pinpointProvider.fetch(entry, ctx);\n} catch (e) {\n  if (String(e.message).startsWith('pinpoint: untrusted hostname')) {\n    logger.warn({ entry: entry.name, host: (() => { try { return new URL(entry.careers_url).hostname; } catch { return '?'; } })() }, 'not a *.pinpointhq.com tenant — use the tenant subdomain, not a vanity domain');\n    return null;\n  }\n  throw e;\n}","preventionTips":["Store the <slug>.pinpointhq.com tenant host in portals.yml, never the vanity careers domain.","Validate all entries against PINPOINT_HOST_RE at config load or in CI.","Treat this error as a potential security signal — confirm who edited the URL before 'fixing' it blindly.","Keep redirect:'error' in fetchText; never bypass the hostname assertion with redirects."],"tags":["ssrf-guard","url-validation","config","pinpoint"],"backgroundTag":"invalid-url","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}