Skip to content

Commit 52e625b

Browse files
sdbrother0claude
andcommitted
[Fix] Handle escaped quotes and invalid URLs in WWW-Authenticate parsing
A quoted auth-param can contain an escaped quote or a comma (RFC 9110 §5.6.4), which broke the split before a later resource_metadata. The header is now split by a loop that tracks quoted strings and escapes. An invalid resource_metadata URL no longer stops parsing, so a later Bearer challenge with a valid URL is still used. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent dd3bd5a commit 52e625b

2 files changed

Lines changed: 28 additions & 8 deletions

File tree

‎src/services/mcp/utils/__tests__/oauth.spec.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ describe("fetchOAuthAuthServerMetadata", () => {
5757
'Bearer RESOURCE_METADATA="https://example.com/core/.well-known/oauth-protected-resource/mcp"',
5858
'bearer error="invalid_token", resource_metadata = "https://example.com/core/.well-known/oauth-protected-resource/mcp"',
5959
'Basic realm="mcp", Bearer resource_metadata="https://example.com/core/.well-known/oauth-protected-resource/mcp"',
60+
'Bearer error_description="bad \\"token\\", retry", resource_metadata="https://example.com/core/.well-known/oauth-protected-resource/mcp"',
61+
'Bearer resource_metadata="not a url", Bearer resource_metadata="https://example.com/core/.well-known/oauth-protected-resource/mcp"',
6062
])("falls back to the resource_metadata URL from the 401 header: %s", async (wwwAuthenticate) => {
6163
vi.mocked(discoverOAuthProtectedResourceMetadata)
6264
.mockRejectedValueOnce(

‎src/services/mcp/utils/oauth.ts‎

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ async function probeResourceMetadataUrl(serverUrl: string): Promise<URL | undefi
170170
}
171171

172172
/** An auth-param, optionally preceded by the auth-scheme that starts its challenge. */
173-
const AUTH_PARAM = /^\s*(?:([^\s="]+)\s+)?([^\s="]+)\s*=\s*(?:"([^"]*)"|([^\s"]*))\s*$/
173+
const AUTH_PARAM = /^\s*(?:([^\s="]+)\s+)?([^\s="]+)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s"]*))\s*$/
174174

175175
/**
176176
* Reads the `resource_metadata` parameter of a Bearer challenge. Unlike the SDK
@@ -180,18 +180,36 @@ const AUTH_PARAM = /^\s*(?:([^\s="]+)\s+)?([^\s="]+)\s*=\s*(?:"([^"]*)"|([^\s"]*
180180
function extractResourceMetadataUrl(header: string | null): URL | undefined {
181181
if (!header) return undefined
182182
let scheme: string | undefined
183-
// Commas outside quoted strings separate challenges and their auth-params.
184-
for (const item of header.match(/(?:[^,"]|"[^"]*")+/g) ?? []) {
183+
for (const item of splitAuthItems(header)) {
185184
const param = AUTH_PARAM.exec(item)
186185
const itemScheme = param ? param[1] : item.trim().split(/\s+/)[0]
187186
if (itemScheme) scheme = itemScheme.toLowerCase()
188187
if (scheme === "bearer" && param?.[2].toLowerCase() === "resource_metadata") {
189-
try {
190-
return new URL(param[3] ?? param[4])
191-
} catch {
192-
return undefined
193-
}
188+
const value = param[3]?.replace(/\\(.)/g, "$1") ?? param[4]
189+
if (URL.canParse(value)) return new URL(value)
194190
}
195191
}
196192
return undefined
197193
}
194+
195+
/**
196+
* Splits a WWW-Authenticate value on commas outside quoted strings. The commas
197+
* separate challenges and their auth-params. A loop keeps this linear in the
198+
* header length and handles escapes inside quoted strings (RFC 9110 §5.6.4).
199+
*/
200+
function splitAuthItems(header: string): string[] {
201+
const items: string[] = []
202+
let start = 0
203+
let quoted = false
204+
for (let i = 0; i < header.length; i++) {
205+
const char = header[i]
206+
if (quoted && char === "\\") i++
207+
else if (char === '"') quoted = !quoted
208+
else if (char === "," && !quoted) {
209+
items.push(header.slice(start, i))
210+
start = i + 1
211+
}
212+
}
213+
items.push(header.slice(start))
214+
return items
215+
}

0 commit comments

Comments
 (0)