From e8596cf0d3d4c4a27b7be28ed3a855ea59908b8d Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Wed, 4 Nov 2026 00:07:21 +0000 Subject: [PATCH] fix(mcp): Let browsers send Origin on the consent POST The consent page set Referrer-Policy: no-referrer. Under that policy, browsers send Origin: null on a form POST, so the same-origin check rejected every Allow click with "Request blocked". Use same-origin instead: the consent POST keeps its Origin, and the cross-origin redirect to the client still gets no referrer. The test fixture now derives Origin from the page's referrer policy the way a browser does, so the integration test catches this. Co-Authored-By: David Cramer <23610+dcramer@users.noreply.github.com> --- packages/junior/src/api/mcp/oauth/page.ts | 10 ++++++-- packages/junior/tests/fixtures/mcp-http.ts | 24 +++++++++++++++---- .../tests/integration/api/mcp/route.test.ts | 1 + 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/packages/junior/src/api/mcp/oauth/page.ts b/packages/junior/src/api/mcp/oauth/page.ts index 096ee82a91..79730a9800 100644 --- a/packages/junior/src/api/mcp/oauth/page.ts +++ b/packages/junior/src/api/mcp/oauth/page.ts @@ -1,6 +1,9 @@ import { escapeHtml, renderHtmlPage } from "@/api/html"; -/** Render one MCP sign-in page. It sends no referrer and loads no resources. */ +/** + * Render one MCP sign-in page. It loads no resources and sends no referrer to + * other origins. + */ export function renderMcpPage(page: { /** HTML that follows the message. Callers escape every value in it. */ bodyHtml?: string; @@ -19,7 +22,10 @@ export function renderMcpPage(page: { bodyHtml: page.bodyHtml ? `${message}\n${page.bodyHtml}` : message, headers: { "content-security-policy": `default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'; form-action ${formAction.join(" ")}`, - "referrer-policy": "no-referrer", + // Not `no-referrer`: browsers then send `Origin: null` on the consent + // POST, which fails the same-origin check. The client redirect is + // cross-origin, so it still gets no referrer. + "referrer-policy": "same-origin", }, status: page.status, title: page.title, diff --git a/packages/junior/tests/fixtures/mcp-http.ts b/packages/junior/tests/fixtures/mcp-http.ts index f28f2ca29b..012dbc33fb 100644 --- a/packages/junior/tests/fixtures/mcp-http.ts +++ b/packages/junior/tests/fixtures/mcp-http.ts @@ -111,7 +111,12 @@ export async function openMcpConsentPage( app: Hono, consentUrl: string, email: string, -): Promise<{ csrf?: string; html: string; status: number }> { +): Promise<{ + csrf?: string; + html: string; + referrerPolicy: string | null; + status: number; +}> { const response = await app.request(consentUrl, { headers: { cookie: viewerCookie(email) }, }); @@ -119,21 +124,31 @@ export async function openMcpConsentPage( return { csrf: html.match(/name="csrf" value="([^"]+)"/)?.[1], html, + referrerPolicy: response.headers.get("referrer-policy"), status: response.status, }; } -/** Approve consent as the viewer who opened the page. */ +/** + * Approve consent as the viewer who opened the page. The `Origin` header + * follows the page's referrer policy the way a browser form POST does: under + * `no-referrer`, browsers send `Origin: null`. + */ export async function approveMcpConsent( app: Hono, - args: { consentUrl: string; csrf: string; email: string }, + args: { + consentUrl: string; + csrf: string; + email: string; + referrerPolicy: string | null; + }, ): Promise { return await app.request(args.consentUrl, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded", cookie: viewerCookie(args.email), - origin: MCP_TEST_ORIGIN, + origin: args.referrerPolicy === "no-referrer" ? "null" : MCP_TEST_ORIGIN, }, body: new URLSearchParams({ action: "approve", csrf: args.csrf }), }); @@ -172,6 +187,7 @@ export async function approveMcpClient( consentUrl, csrf: page.csrf ?? "", email, + referrerPolicy: page.referrerPolicy, }); const code = new URL(approved.headers.get("location") ?? "").searchParams.get( "code", diff --git a/packages/junior/tests/integration/api/mcp/route.test.ts b/packages/junior/tests/integration/api/mcp/route.test.ts index 428cb860eb..3635a18e2e 100644 --- a/packages/junior/tests/integration/api/mcp/route.test.ts +++ b/packages/junior/tests/integration/api/mcp/route.test.ts @@ -110,6 +110,7 @@ describe("remote MCP HTTP", () => { consentUrl, csrf: page.csrf ?? "", email: MCP_TEST_VIEWER, + referrerPolicy: page.referrerPolicy, }); const callback = new URL(approved.headers.get("location") ?? ""); const issued = await exchangeMcpCode(app, {