Skip to content

fix: escape html in external oauth error message#841

Merged
mattzcarey merged 7 commits intomainfrom
fix/xss-oauth-callback-v2
Feb 4, 2026
Merged

fix: escape html in external oauth error message#841
mattzcarey merged 7 commits intomainfrom
fix/xss-oauth-callback-v2

Conversation

@mattzcarey
Copy link
Contributor

No description provided.

@changeset-bot
Copy link

changeset-bot bot commented Feb 4, 2026

🦋 Changeset detected

Latest commit: b636122

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
agents Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@changeset-bot
Copy link

changeset-bot bot commented Feb 4, 2026

🦋 Changeset detected

Latest commit: c3ff8bc

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
agents Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@claude
Copy link

claude bot commented Feb 4, 2026

Claude Code Review

Security Fix: XSS Prevention in OAuth Error Messages

This PR addresses a critical XSS vulnerability in OAuth error handling. The implementation is solid with good test coverage.

Issues Found:

  1. Missing error field initialization in placeholder (examples/mcp-client/src/client.tsx:72)

    • The placeholder object adds error: null but the type definition doesn't include it
    • Need to verify MCPServer type includes the error field or add it
  2. Inconsistent error handling strategy (packages/agents/src/mcp/client.ts:694-716)

    • Lines 694-716 throw errors that bypass the failConnection() helper
    • Consider whether these early validation errors should also be stored in connectionError for consistency
    • Current approach: throw for unidentifiable connections, store error for identifiable ones
    • This distinction makes sense but deserves a comment explaining the rationale
  3. Type safety gap (packages/agents/src/index.ts:262)

    • Added error: string | null to MCPServer type
    • But connectionError field in MCPClientConnection is used without being formally defined in a shared type
    • Consider creating a shared error state type

Strengths:

  • Comprehensive XSS test coverage (2 test cases for different attack vectors)
  • Proper HTML escaping using escape-html library
  • Clean refactor that removes insecure script-based error display
  • Error state now properly tracked in connection for UI display
  • Good separation of concerns: early validation throws, connection errors are stored

Minor:

  • The changeset description is accurate and clear

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 4, 2026

Open in StackBlitz

npm i https://pkg.pr.new/cloudflare/agents@841

commit: b636122

@mattzcarey
Copy link
Contributor Author

happy with this now :)

…ion errors (throw) from connection-specific errors (fail connection)

Move all connection-related error handling into try-catch block to ensure consistent error reporting. Escape OAuth error params to prevent XSS. Update tests to expect failed connections instead of thrown errors for connection-specific validation failures.
agents-git-bot bot pushed a commit to cloudflare/cloudflare-docs that referenced this pull request Feb 4, 2026
Updates documentation to reflect security improvements in OAuth error handling:

- Remove MCPClientOAuthResult from customHandler signature (no longer receives error info)
- Document new `error` field in MCPServer type that stores connection errors
- Update examples to display errors from connection state instead of script alerts
- Add note that error messages are automatically escaped to prevent XSS attacks
- Simplify customHandler examples to only close popup (errors handled separately)

Related to cloudflare/agents#841

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
status: 200
});
} else {
const safeError = JSON.stringify(result.authError || "Unknown error");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

remove this hot garbage

export class MCPClientConnection {
client: Client;
connectionState: MCPConnectionState = MCPConnectionState.CONNECTING;
connectionError: string | null = null;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

add this string as a first class thing which can be bubbled up

@@ -1,4 +1,5 @@
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
import escapeHtml from "escape-html";
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this library is already in the bundle as a transient dep so thats handy

/**
* Result of an OAuth callback request
*/
export type MCPOAuthCallbackResult =
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this type didnt exist but it was nice to define it

);
}

private failConnection(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this standardises what happens when we have a connection so we know which one to mark as failed. extracting this logic means I dont have to repeat it for the 3 occasions this happens in

Comment on lines 694 to 716
@@ -684,7 +705,6 @@ export class MCPClientManager {

const servers = this.getServersFromStorage();
const serverExists = servers.some((server) => server.id === serverId);

if (!serverExists) {
throw new Error(
`No server found with id "${serverId}". Was the request matched with \`isCallbackRequest()\`?`
@@ -695,89 +715,61 @@ export class MCPClientManager {
throw new Error(`Could not find serverId: ${serverId}`);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

in these cases we cant get a connection id to mark the connection as failed so not much choice other to error out. In future this might change.


const authProvider = conn.options.transport.authProvider;
authProvider.serverId = serverId;
try {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

from here we have a connection id so any errors are wrapped in this try catch and we can fail the connection gracefully.

}
if (error) {
// Escape external OAuth error params to prevent XSS
throw new Error(escapeHtml(errorDescription || error));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

the big baddie xss

Comment on lines +747 to +754
// Already authenticated - just return success
if (
conn.connectionState === MCPConnectionState.READY ||
conn.connectionState === MCPConnectionState.CONNECTED
) {
this.clearServerAuthUrl(serverId);
return { serverId, authSuccess: true };
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

happy path making sure to clear the auth url. Maybe we also need to clear the auth error also but it shouldnt be set.

Comment on lines 762 to 767
await authProvider.consumeState(state);
await conn.completeAuthorization(code);
await authProvider.deleteCodeVerifier();
this.clearServerAuthUrl(serverId);
conn.connectionError = null;
this._onServerStateChanged.fire();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

other happy path.

return { serverId, authSuccess: true };
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return this.failConnection(serverId, message);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

extracted logic

});

it("should throw error for callback without code or error", async () => {
it("should fail connection for callback without code or error", async () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this makes sense since we can tie it to a connection id. This would be the result of a server misbehaving so we wouldnt want to get stuck in authentication

});

it("should error when callback received for connection in failed state", async () => {
it("should fail connection when callback received for connection in failed state", async () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

same here, developer should manually flip the connection state if they want to initiate a retry

@mattzcarey mattzcarey merged commit 3f490d0 into main Feb 4, 2026
32 checks passed
@mattzcarey mattzcarey deleted the fix/xss-oauth-callback-v2 branch February 4, 2026 21:16
@github-actions github-actions bot mentioned this pull request Feb 4, 2026
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