Skip to content

fix: OAuth connector helpers must enforce the connector's host allowlist before attaching credentials - #313

Merged
kentcdodds merged 2 commits into
mainfrom
security/group-2-oauth-token-egress
May 1, 2026
Merged

kentcdodds merged 2 commits into
mainfrom
security/group-2-oauth-token-egress

Conversation

@kentcdodds

@kentcdodds kentcdodds commented May 1, 2026 •

Copy link
Copy Markdown
Owner

Summary

Fixes a HIGH-severity finding where createAuthenticatedFetch (and its sandboxed prelude equivalent) would attach a materialized OAuth bearer token to outbound requests targeting any absolute URL, allowing sandboxed packages or generated UIs to exfiltrate connected OAuth tokens to arbitrary attacker-controlled hosts.

Changes

New: connector-host-allowlist.ts

Reusable enforcement logic extracted into a focused module:

  • ConnectorHostNotAllowedError — typed error thrown when a request targets a disallowed host
  • getConnectorAllowedHosts(connector) — resolves the full set of normalized allowed hosts from requiredHosts + apiBaseUrl
  • assertConnectorHostAllowed(connectorName, connector, url) — pre-flight assertion that throws before any credential attachment

Modified: codemode-utils.ts

  • createAuthenticatedFetch now calls assertConnectorHostAllowed on the resolved URL before attaching the Authorization header. If the host is not in the connector's allowlist, the request never fires.
  • The sandboxed prelude (createExecuteHelperPrelude) includes an equivalent inline implementation so module-mode packages get the same enforcement.
  • refreshAccessToken is annotated @internal with a JSDoc comment explaining the security boundary: callers that use the raw token must enforce the host allowlist themselves.

New tests: authenticated-fetch.node.test.ts

  • Verifies that a request to a disallowed host (attacker.example) is rejected with ConnectorHostNotAllowedError and fetch is never called.
  • Verifies that requests to apiBaseUrl host and requiredHosts entries succeed with the bearer attached.
  • Verifies the error message does not leak the token value.
  • Covers both the native export and the sandboxed prelude version.
  • Verifies fail-closed behavior when connector has no allowlist configured.
  • Verifies protocol-relative URLs (//evil.com/steal) are blocked.

Docs: docs/contributing/architecture/index.md

Documents the helper-level allowlist invariant for future contributors.

Security Model

The fetch gateway (fetch-gateway.ts) enforces host allowlists only for secret placeholders ({{secret:NAME}}). Once a token is materialized into a raw string (as refreshAccessToken does), the gateway cannot see it. The fix enforces the same connector allowlist at the helper layer, which is the only code path that materializes and attaches connector tokens to outbound requests.

Defense-in-depth measures

  • Fail-closed on empty allowlist: If a connector has neither requiredHosts nor apiBaseUrl configured, the helper throws rather than silently allowing all hosts.
  • Protocol-relative URL handling: URLs like //evil.com/steal are resolved with a dummy scheme to extract the host for allowlist checking, preventing bypass through this vector.
Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Enforced host allowlisting for outbound authenticated requests: tokens are not attached to disallowed hosts and such requests are blocked with a safe error that omits token values.
  • Tests

    • Added comprehensive tests covering allowlist enforcement, allowed vs disallowed requests, relative URL handling, and fail-closed behavior when no hosts are configured.
  • Documentation

    • Added guidance on OAuth token security and required host allowlist behavior.

@coderabbitai

coderabbitai Bot commented May 1, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6436b9a2-8a6b-4646-af13-ddf454142bf3

📥 Commits

Reviewing files that changed from the base of the PR and between 8264cc3 and d9af8ed.

📒 Files selected for processing (4)
  • docs/contributing/architecture/index.md
  • packages/worker/src/mcp/execute-modules/authenticated-fetch.node.test.ts
  • packages/worker/src/mcp/execute-modules/codemode-utils.ts
  • packages/worker/src/mcp/execute-modules/connector-host-allowlist.ts
✅ Files skipped from review due to trivial changes (1)
  • docs/contributing/architecture/index.md

📝 Walkthrough

Walkthrough

Adds a connector host allowlist mechanism, enforces it before materializing/attaching OAuth tokens in authenticated fetch paths (including the prelude sandbox), provides helpers and a specific error type, adds tests covering allowed/disallowed flows, and documents the requirement.

Changes

Cohort / File(s) Summary
Documentation
docs/contributing/architecture/index.md
Adds documentation specifying that token materialization requires host allowlisting, the computation of allowed hosts (from requiredHosts + apiBaseUrl host), failure behavior (throw ConnectorHostNotAllowedError, no network request, omit token values), and helper locations.
Allowlist Core
packages/worker/src/mcp/execute-modules/connector-host-allowlist.ts
New module introducing ConnectorHostNotAllowedError, getConnectorAllowedHosts(connector), and assertConnectorHostAllowed(connectorName, connector, url); normalizes hosts, unions requiredHosts with apiBaseUrl host, skips invalid apiBaseUrl, treats relative paths as non-enforced, and fails closed when allowlist empty.
Authenticated Fetch Integration
packages/worker/src/mcp/execute-modules/codemode-utils.ts
Integrates allowlist checks into createAuthenticatedFetch and the execute-helper prelude: resolve final URL, call assertConnectorHostAllowed(...) before building the Request or attaching Authorization, re-exports ConnectorHostNotAllowedError, and documents refreshAccessToken token-materialization caveat.
Tests
packages/worker/src/mcp/execute-modules/authenticated-fetch.node.test.ts
Adds Vitest suite that stubs token endpoint and outbound fetches; verifies disallowed hosts throw ConnectorHostNotAllowedError (or sandbox rejection) with no outbound request nor token leakage, allowed hosts receive Authorization: Bearer <token>, protocol-relative/relative path behaviors, and fails-closed when allowlist empty.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant AuthFetch as createAuthenticatedFetch
    participant Allowlist as Connector Host Allowlist
    participant TokenSvc as OAuth Token Endpoint
    participant Destination as Destination API

    Client->>AuthFetch: request via authenticated fetch (url, connectorConfig)
    AuthFetch->>AuthFetch: resolve final URL
    AuthFetch->>Allowlist: assertConnectorHostAllowed(connectorName, connector, resolvedURL)
    alt host allowed
        Allowlist-->>AuthFetch: allowed (host in allowed set)
        AuthFetch->>TokenSvc: fetch access token
        TokenSvc-->>AuthFetch: return access token
        AuthFetch->>Destination: outbound request with Authorization: Bearer <token>
        Destination-->>AuthFetch: response
        AuthFetch-->>Client: response
    else host not allowed
        Allowlist-->>AuthFetch: throw ConnectorHostNotAllowedError (no token disclosed)
        AuthFetch-->>Client: error (no network call to Destination)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through hosts both near and far,

I sniffed the headers, found each hidden star.
Tokens safe in burrowed ground,
Only allowed hosts wear the crown—
Hooray for checks that keep secrets sound!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main security fix: enforcing host allowlisting before credential attachment in OAuth connector helpers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch security/group-2-oauth-token-egress

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@kentcdodds
kentcdodds marked this pull request as ready for review May 1, 2026 20:01
@github-actions

github-actions Bot commented May 1, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-313.kentcdodds.workers.dev

Worker: kody-pr-313
D1: kody-pr-313-db
KV: kody-pr-313-oauth-kv

Mocks:

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/worker/src/mcp/execute-modules/connector-host-allowlist.ts`:
- Around line 78-79: The current code in connector-host-allowlist.ts returns
early when getConnectorAllowedHosts(connector) yields an empty array, leaving
the connector in a fail-open state; instead, when allowedHosts.length === 0 you
must reject the configuration (throw an Error) so misconfigured connectors fail
closed. Update the logic in connector-host-allowlist.ts to throw a descriptive
error when getConnectorAllowedHosts(connector) returns an empty list
(referencing getConnectorAllowedHosts, createAuthenticatedFetch and
ConnectorConfig) and make the identical change in the mirrored
prelude/codemode-utils.ts copy so both code paths enforce a non-empty allowlist
rather than returning silently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7f22c022-214e-4e8f-8088-7b9652b9a1b5

📥 Commits

Reviewing files that changed from the base of the PR and between 1e00b08 and 8264cc3.

📒 Files selected for processing (4)
  • docs/contributing/architecture/index.md
  • packages/worker/src/mcp/execute-modules/authenticated-fetch.node.test.ts
  • packages/worker/src/mcp/execute-modules/codemode-utils.ts
  • packages/worker/src/mcp/execute-modules/connector-host-allowlist.ts

Comment thread packages/worker/src/mcp/execute-modules/connector-host-allowlist.ts Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8264cc3. Configure here.

Comment thread packages/worker/src/mcp/execute-modules/connector-host-allowlist.ts
OAuth connector helpers (createAuthenticatedFetch and the sandboxed prelude
equivalent) now assert that the outbound request URL targets a host listed
in the connector's requiredHosts or derived from its apiBaseUrl before
attaching the bearer token.

- Add ConnectorHostNotAllowedError and reusable assertConnectorHostAllowed /
  getConnectorAllowedHosts helpers in connector-host-allowlist.ts.
- Enforce the allowlist in both the native createAuthenticatedFetch and the
  sandboxed __kodyCreateAuthenticatedFetch prelude code.
- Mark refreshAccessToken @internal with JSDoc documenting the security
  boundary (materialized token bypasses the fetch gateway's placeholder-based
  host check).
- Add unit tests in authenticated-fetch.node.test.ts covering rejection for
  disallowed hosts, success for allowed hosts, and token non-leakage in error
  messages.
- Document the helper-level allowlist invariant in
  docs/contributing/architecture/index.md.
- assertConnectorHostAllowed now throws when the connector has no allowed
  hosts configured (requiredHosts and apiBaseUrl both empty) instead of
  silently allowing all requests.
- Protocol-relative URLs (//evil.com/steal) are resolved with a dummy
  scheme before host extraction, preventing bypass of the allowlist check.
- Both the native module and the sandboxed prelude are updated.
- Added tests for both edge cases.
@cursor
cursor Bot force-pushed the security/group-2-oauth-token-egress branch from c6aa525 to d9af8ed Compare May 1, 2026 21:16
@kentcdodds
kentcdodds merged commit 6a08939 into main May 1, 2026
9 checks passed
@kentcdodds
kentcdodds deleted the security/group-2-oauth-token-egress branch May 1, 2026 22:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants