santifer/career-ops · error · Error

bamboohr: untrusted hostname "${parsed.hostname}" — must mat

Error message

bamboohr: untrusted hostname "${parsed.hostname}" — must match <tenant>.bamboohr.com

What it means

The host-allowlist step of BambooHR's SSRF guard. The hostname must match `/^[a-z0-9][a-z0-9-]*\.bamboohr\.com$/` — i.e. a single lowercase tenant label (alphanumeric + hyphens, not starting with a hyphen) followed by `.bamboohr.com`. Uppercase, multi-label tenants, or other domains are rejected.

Source

Thrown at providers/bamboohr.mjs:27

//
// The list endpoint (`/careers/list`) returns lightweight metadata — enough for
// the Job contract (title, url, location) at zero token cost. The full JD lives
// behind a second `/careers/<id>/detail` request, which the scanner deliberately
// skips to stay zero-token (so `description`/`postedAt` are omitted).

const BAMBOOHR_HOST_RE = /^[a-z0-9][a-z0-9-]*\.bamboohr\.com$/;

/** @param {string} url */
function assertBambooHRUrl(url) {
  let parsed;
  try {
    parsed = new URL(url);
  } catch {
    throw new Error(`bamboohr: invalid URL: ${url}`);
  }
  if (parsed.protocol !== 'https:') throw new Error(`bamboohr: URL must use HTTPS: ${url}`);
  if (!BAMBOOHR_HOST_RE.test(parsed.hostname)) {
    throw new Error(`bamboohr: untrusted hostname "${parsed.hostname}" — must match <tenant>.bamboohr.com`);
  }
  return url;
}

/**
 * Resolve the tenant origin (`https://<tenant>.bamboohr.com`) from an entry.
 * Honours an explicit `api:` URL, else parses `careers_url`.
 * @param {import('./_types.js').PortalEntry} entry
 * @returns {string | null}
 */
function resolveOrigin(entry) {
  const rawApi = typeof entry.api === 'string' ? entry.api : '';
  const rawCareers = typeof entry.careers_url === 'string' ? entry.careers_url : '';
  const raw = (rawApi || rawCareers).trim();
  if (!raw) return null;
  let parsed;
  try {
    parsed = new URL(raw);

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Use a single lowercase tenant label: `https://acme.bamboohr.com`.
  2. If the tenant legitimately has subdomains, lower-case the host and adjust the regex to permit extra labels only if you trust them.
  3. Confirm the host is actually a BambooHR tenant before assigning the provider.

Example fix

# before (rejected — uppercase + subdomain)
- name: Acme
  api: https://Jobs.Acme.bamboohr.com

# after
- name: Acme
  api: https://acme.bamboohr.com
Defensive patterns

Strategy: validation

Validate before calling

const RE = /^[a-z0-9][a-z0-9-]*\.bamboohr\.com$/;
function assertBambooHost(u) {
  const h = new URL(u).hostname;
  if (!RE.test(h)) throw new Error(`bamboohr: host ${h} must match <tenant>.bamboohr.com (lowercase, single label)`);
}
if (entry.api) assertBambooHost(entry.api);
if (entry.careers_url) assertBambooHost(entry.careers_url);

Prevention

When it happens

Trigger: `BAMBOOHR_HOST_RE.test(parsed.hostname)` is false: hostname is uppercase (`Acme.bamboohr.com`), has a multi-part tenant (`jobs.acme.bamboohr.com`), uses a different domain, or is the apex (`bamboohr.com`).

Common situations: A tenant name with uppercase letters, a deep subdomain, a typo in the domain, or a URL pointing at a non-BambooHR host.

Related errors


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/1de9726e94688110. Report an issue: GitHub.