santifer/career-ops · error · Error

workable: untrusted hostname

Error message

workable: untrusted hostname "${parsed.hostname}" — must be one of: ${[...ALLOWED_WORKABLE_HOSTS].join(', ')}

What it means

assertWorkableUrl enforces a hostname allowlist (ALLOWED_WORKABLE_HOSTS) after the HTTPS check. Any URL whose parsed hostname is not in that set is rejected to prevent SSRF — a configured or derived URL pointing at an arbitrary server is refused before any fetch happens.

Solutions

  1. Point the URL at an allowlisted Workable host (see the list in the message)
  2. Fix typos or embedded extra domains in the hostname
  3. If a new legitimate Workable host is genuinely needed, add it to ALLOWED_WORKABLE_HOSTS in providers/workable.mjs after verifying it

Example fix

// before
assertWorkableUrl('https://jobs.acme.com/widget?account=acme');
// after
assertWorkableUrl('https://apply.workable.com/acme/widget');
Defensive patterns

Strategy: validation

Validate before calling

const ALLOWED = ['apply.workable.com','workable.com'];
function isAllowedWorkableHost(url) { try { return ALLOWED.includes(new URL(url).hostname); } catch { return false; } }

Type guard

const isAllowedWorkableHost = (u) => { try { return ['apply.workable.com','workable.com'].includes(new URL(u).hostname); } catch { return false; } };

Try / catch

try { return await workableProvider.fetch(entry, ctx); } catch (e) { if (e.message.includes('untrusted hostname')) { console.warn(`Entry ${entry.name} points at a non-Workable host; check careers_url`); return []; } throw e; }

Prevention

When it happens

Trigger: assertWorkableUrl called with a parsed hostname outside ALLOWED_WORKABLE_HOSTS, e.g. 'https://evil.example.com/acme' or a typo'd host like 'apply.workable.com.evil.io'.

Common situations: A careers_url pointing at a company's own site instead of its Workable board; a hostile/mistyped entry in portals.yml; a custom proxy host that is not allowlisted.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/67106249177ac59b. Report an issue: GitHub.

Appendix: source

Thrown at providers/workable.mjs:118

// same host, so this process never needs more than one in-flight request to
// it at a time.
let workableQueue = Promise.resolve();
function serialized(fn) {
  const result = workableQueue.then(fn, fn);
  workableQueue = result.then(() => undefined, () => undefined);
  return result;
}

function assertWorkableUrl(url) {
  let parsed;
  try {
    parsed = new URL(url);
  } catch {
    throw new Error(`workable: invalid URL: ${url}`);
  }
  if (parsed.protocol !== 'https:') throw new Error(`workable: URL must use HTTPS: ${url}`);
  if (!ALLOWED_WORKABLE_HOSTS.has(parsed.hostname)) {
    throw new Error(`workable: untrusted hostname "${parsed.hostname}" — must be one of: ${[...ALLOWED_WORKABLE_HOSTS].join(', ')}`);
  }
  return url;
}

/**
 * Extract the account slug from a tracked_companies entry's careers_url.
 * @returns {string|null}
 */
export function resolveWorkableSlug(entry) {
  const raw = entry && typeof entry.careers_url === 'string' ? entry.careers_url : '';
  if (!raw) return null;
  let parsed;
  try {
    parsed = new URL(raw);
  } catch {
    return null;
  }
  if (parsed.protocol !== 'https:') return null;

View on GitHub (pinned to aac998c7ed)