Skip to content

fix: restrict workos doctor credential base URL to trusted hosts#199

Open
devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
security/sec-1364-doctor-restrict-credential-base-url
Open

fix: restrict workos doctor credential base URL to trusted hosts#199
devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
security/sec-1364-doctor-restrict-credential-base-url

Conversation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Summary

Automated first-pass security fix for SEC-1364requires human security review before merge.

workos doctor resolves the WorkOS API key from the developer's own secrets (shell env or .env.local) but resolves the API base URL from the project's .env/.env.local — a file an attacker can commit to any repo. It then sent the key as Authorization: Bearer to that URL with no host validation, no HTTPS enforcement, and no consent prompt. A developer running this "read-only" diagnostic inside a poisoned clone leaks their staging/sandbox key (full admin of that WorkOS environment) to an attacker host. Confirmed by reproduction: the key was captured verbatim by an attacker-controlled host, and skipped only for sk_live_ keys / --skip-api.

The fix gates the credentialed call (checkDashboardSettingsfetchDashboardSettings in src/doctor/checks/dashboard.ts) behind a host allowlist so the key is only ever sent to trusted WorkOS hosts:

export function isCredentialSafeBaseUrl(baseUrl: string): boolean {
  const url = tryParse(baseUrl);                 // unparsable -> false
  const host = url.hostname.toLowerCase();
  if (host === 'localhost' || '127.0.0.1' || '::1') return true;  // internal dev, any scheme
  if (url.protocol !== 'https:') return false;   // remote must be HTTPS
  return host === 'workos.com' || host.endsWith('.workos.com');
}

An untrusted WORKOS_BASE_URL now short-circuits with Skipped (untrusted WORKOS_BASE_URL: <url>) in the report instead of attaching the credential. Host matching is on the parsed URL.hostname, so look-alikes like api.workos.com.attacker.example are rejected.

Scope / follow-ups (for the human reviewer)

  • Scoped to the credential-carrying call, which is the SEC-1364 vulnerability. The unauthenticated connectivity probe (checkConnectivity${baseUrl}/health) still uses the resolved base URL and will hit an attacker host with no credential — a much lower-severity SSRF/telemetry leak left out of this minimal fix; consider gating it on the same allowlist.
  • The finding also suggests source-origin tracking (warn when key and base URL come from different files) and an explicit consent prompt for non-default hosts. Not included here to keep the diff small; reasonable hardening follow-ups.

Testing

  • New src/doctor/checks/dashboard.spec.ts: allowlist unit tests (default host, *.workos.com, localhost, look-alike/rejection, non-HTTPS, unparsable) + checkDashboardSettings does not call fetch for an attacker URL, proceeds for a trusted one, and still skips production keys.
  • Local end-to-end repro: vulnerable build leaked the Bearer key to workos-diag.attacker.example; the fixed build produced 0 credential requests and surfaced the skip reason.
  • pnpm tsc --noEmit, pnpm lint, pnpm format:check, and full pnpm test (1995 tests) pass.

Do not merge/approve without human security review.

Link to Devin session: https://app.devin.ai/sessions/a67b9b282a7040c6bacd3ac0437fb152

`workos doctor` resolves the API key from the developer's own secrets but
resolves the API base URL from the project's `.env`/`.env.local`, which an
attacker can commit to a repository. It then sent the key as a Bearer header
to that URL with no host validation, exfiltrating the key to an
attacker-controlled host.

Gate the credentialed dashboard call behind an allowlist: only send the API
key to `*.workos.com` over HTTPS, or to localhost for internal development.
An untrusted `WORKOS_BASE_URL` now skips the call and surfaces a clear reason
in the report instead of leaking the credential.
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author
Original prompt from Linear User

Please work on ticket "workos doctor exfiltrates the resolved WorkOS API key to an attacker-controlled base URL taken from the project's .env file" (SEC-1364)

@playbook:playbook-b588614117c7477a9b9729928385384f

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@linear-code

linear-code Bot commented Jul 22, 2026

Copy link
Copy Markdown

SEC-1364

@devin-ai-integration devin-ai-integration Bot changed the title doctor: Restrict credential base URL to trusted WorkOS hosts fix: restrict workos doctor credential base URL to trusted hosts Jul 22, 2026
@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR prevents workos doctor from sending API credentials to unapproved destinations. The main changes are:

  • Adds an HTTPS WorkOS hostname allowlist with local-development exceptions.
  • Normalizes trailing DNS root-label dots for trusted WorkOS hosts.
  • Skips credentialed dashboard requests when the base URL is not allowed.
  • Adds tests for trusted hosts, look-alikes, invalid URLs, and guarded requests.

Confidence Score: 5/5

This looks safe to merge.

  • The trusted trailing-dot hostname now passes after normalization.
  • Look-alike domains remain outside the allowlist.
  • No blocking issues were found in the updated code.

Important Files Changed

Filename Overview
src/doctor/checks/dashboard.ts Adds the credential destination guard and accepts trusted WorkOS FQDNs with a trailing root-label dot.
src/doctor/checks/dashboard.spec.ts Tests hostname validation and verifies that rejected destinations receive no credentialed request.

Reviews (2): Last reviewed commit: "Normalize trailing-dot FQDN when checkin..." | Re-trigger Greptile

Comment thread src/doctor/checks/dashboard.ts Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

0 participants