santifer/career-ops · error · Error

rippling: untrusted hostname "${parsed.hostname}" — must be

Error message

rippling: untrusted hostname "${parsed.hostname}" — must be ${API_HOST}

What it means

assertRipplingApiUrl throws when parsed.hostname is not exactly API_HOST ('api.rippling.com'). This is a strict single-host SSRF guard: rippling's API lives on one fixed host, so any other hostname — including a careers host like ats.rippling.com or a branded domain — is rejected at the API layer.

Source

Thrown at providers/rippling.mjs:58

  return segment;
}

/** Build the board API URL for a validated slug. */
function apiUrlForSlug(slug) {
  return `${API_BASE}/${encodeURIComponent(slug)}/jobs`;
}

/** @param {string} url */
function assertRipplingApiUrl(url) {
  let parsed;
  try {
    parsed = new URL(url);
  } catch {
    throw new Error(`rippling: invalid URL: ${url}`);
  }
  if (parsed.protocol !== 'https:') throw new Error(`rippling: URL must use HTTPS: ${url}`);
  if (parsed.hostname !== API_HOST) {
    throw new Error(`rippling: untrusted hostname "${parsed.hostname}" — must be ${API_HOST}`);
  }
  return url;
}

/** @type {Provider} */
export default {
  id: 'rippling',

  detect(entry) {
    const slug = resolveSlug(entry);
    return slug ? { url: apiUrlForSlug(slug) } : null;
  },

  async fetch(entry, ctx) {
    const slug = resolveSlug(entry);
    if (!slug) throw new Error(`rippling: cannot derive API URL for ${entry.name}`);
    const apiUrl = apiUrlForSlug(slug);
    assertRipplingApiUrl(apiUrl);

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Always build the API URL via apiUrlForSlug(slug), which uses API_BASE on api.rippling.com.
  2. Confirm API_HOST is 'api.rippling.com' and was not overridden.
  3. Ensure the slug passed SLUG_RE before URL construction so no host-redirecting characters slip through.
  4. Do not pass the careers_url (ats.rippling.com) into assertRipplingApiUrl — pass the derived API URL.

Example fix

// before — careers URL misused as API URL
assertRipplingApiUrl('https://ats.rippling.com/acme/jobs');
// after — derived API URL on the correct host
assertRipplingApiUrl(apiUrlForSlug('acme'));
Defensive patterns

Strategy: validation

Validate before calling

const API_HOST = 'api.rippling.com';
function isRipplingApiUrl(url) {
  try { return new URL(url).hostname === API_HOST; } catch { return false; }
}
const apiUrl = apiUrlForSlug(slug);
if (!isRipplingApiUrl(apiUrl)) {
  throw new Error('rippling: derived API URL is not on api.rippling.com');
}

Type guard

null

Try / catch

try {
  await provider.fetch(entry, ctx);
} catch (e) {
  if (/untrusted hostname/.test(e.message)) {
    console.error('[bug] rippling API URL on wrong host — use apiUrlForSlug, not the careers URL');
  } else throw e;
}

Prevention

When it happens

Trigger: The API URL was constructed pointing at ats.rippling.com (the careers host) instead of api.rippling.com; a redirect or config change swapped the host; a test or external caller passed a URL for a different host.

Common situations: apiUrlForSlug was bypassed and the careers URL was used directly; the API_HOST constant was changed; a slug injection attempted path traversal to a different host.

Related errors


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