Skip to content

Commit da31061

Browse files
committed
fix: match WWW-Authenticate params exactly
1 parent e942d00 commit da31061

2 files changed

Lines changed: 42 additions & 7 deletions

File tree

src/mcp/client/auth/utils.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,25 @@ def extract_field_from_www_auth(response: Response, field_name: str) -> str | No
2626
if not www_auth_header:
2727
return None
2828

29-
# Pattern matches: field_name="value" or field_name=value (unquoted)
30-
pattern = rf'{field_name}=(?:"([^"]+)"|([^\s,]+))'
31-
match = re.search(pattern, www_auth_header)
32-
33-
if match:
34-
# Return quoted value if present, otherwise unquoted value
35-
return match.group(1) or match.group(2)
29+
header_value = www_auth_header.strip()
30+
auth_params = re.sub(r"^\S+\s+", "", header_value, count=1)
31+
32+
# Split auth-params on commas, but only outside quoted values.
33+
in_quotes = False
34+
param_start = 0
35+
for index, char in enumerate(auth_params + ","):
36+
if char == '"':
37+
in_quotes = not in_quotes
38+
elif char == "," and not in_quotes:
39+
param = auth_params[param_start:index].strip()
40+
param_start = index + 1
41+
42+
name, separator, value = param.partition("=")
43+
if separator and name.strip() == field_name:
44+
match = re.match(r'\s*(?:"([^"]+)"|([^\s,]+))', value)
45+
if match:
46+
# Return quoted value if present, otherwise unquoted value
47+
return match.group(1) or match.group(2)
3648

3749
return None
3850

tests/client/test_auth.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1997,6 +1997,16 @@ class TestWWWAuthenticate:
19971997
"scope",
19981998
"admin:write resource:read",
19991999
),
2000+
(
2001+
'Bearer error_scope="decoy", scope="read write"',
2002+
"scope",
2003+
"read write",
2004+
),
2005+
(
2006+
'Bearer realm="api, scope=decoy", scope="read write"',
2007+
"scope",
2008+
"read write",
2009+
),
20002010
(
20012011
'Bearer realm="api", resource_metadata="https://api.example.com/.well-known/oauth-protected-resource", '
20022012
'error="insufficient_scope"',
@@ -2047,6 +2057,19 @@ def test_extract_field_from_www_auth_valid_cases(
20472057
# Header without requested field
20482058
('Bearer realm="api", error="insufficient_scope"', "scope", "no scope parameter"),
20492059
('Bearer realm="api", scope="read write"', "resource_metadata", "no resource_metadata parameter"),
2060+
('Bearer custom_scope="leaked"', "scope", "substring auth-param should not match scope"),
2061+
('Bearer realm="api scope=leaked"', "scope", "auth-param inside quoted value should not match scope"),
2062+
('Bearer realm="api, scope=leaked"', "scope", "auth-param after quoted comma should not match scope"),
2063+
(
2064+
'Bearer x_resource_metadata="https://decoy.example.com"',
2065+
"resource_metadata",
2066+
"substring auth-param should not match resource_metadata",
2067+
),
2068+
(
2069+
'Bearer realm="api, resource_metadata=https://decoy.example.com"',
2070+
"resource_metadata",
2071+
"auth-param after quoted comma should not match resource_metadata",
2072+
),
20502073
# Malformed field (empty value)
20512074
("Bearer scope=", "scope", "malformed scope parameter"),
20522075
("Bearer resource_metadata=", "resource_metadata", "malformed resource_metadata parameter"),

0 commit comments

Comments
 (0)