Fix substring matches in WWW-Authenticate parsing#3041
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes a correctness issue in extract_field_from_www_auth where an auth-param name could be matched as a substring of a different param name (e.g., scope incorrectly matching inside error_scope), and adds regression coverage to prevent reintroducing the bug.
Changes:
- Anchor auth-param name matching to the start of the header or a parameter separator to avoid substring shadowing.
- Escape
field_namein the regex to ensure literal matching. - Add regression test cases covering both “shadowing” (real param present but a decoy exists) and “false positive” (only decoy param exists).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
src/mcp/client/auth/utils.py |
Updates extract_field_from_www_auth regex to require a real auth-param name boundary and escape the field name. |
tests/client/test_auth.py |
Adds regression cases ensuring substring-shadowing and substring-only params no longer match. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
1 issue found across 2 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
There was a problem hiding this comment.
2 issues found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/mcp/client/auth/utils.py">
<violation number="1" location="src/mcp/client/auth/utils.py:50">
P2: When a `WWW-Authenticate` header contains more than one Bearer challenge, the parser currently keeps overwriting the collected auth params and ends up using the last Bearer challenge. That can make scope/resource metadata extraction come from a different challenge than intended. The overwrite happens because every `Bearer ...` segment reinitializes `auth_params`; stopping at the first Bearer challenge (or only initializing once) would keep behavior consistent with the function contract.</violation>
<violation number="2" location="src/mcp/client/auth/utils.py:56">
P2: Bearer params after a valid `auth-param` that uses optional whitespace before `=` can be skipped, so fields later in the same challenge may return `None`. The break condition is classifying `scope = "..."`-style segments as a new challenge; consider excluding segments where the remainder starts with `=`.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
| if separator and "=" not in scheme: | ||
| break |
There was a problem hiding this comment.
P2: Bearer params after a valid auth-param that uses optional whitespace before = can be skipped, so fields later in the same challenge may return None. The break condition is classifying scope = "..."-style segments as a new challenge; consider excluding segments where the remainder starts with =.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/mcp/client/auth/utils.py, line 56:
<comment>Bearer params after a valid `auth-param` that uses optional whitespace before `=` can be skipped, so fields later in the same challenge may return `None`. The break condition is classifying `scope = "..."`-style segments as a new challenge; consider excluding segments where the remainder starts with `=`.</comment>
<file context>
@@ -16,6 +16,52 @@
+ continue
+
+ if collecting:
+ if separator and "=" not in scheme:
+ break
+ auth_params.append(segment)
</file context>
| if separator and "=" not in scheme: | |
| break | |
| if separator and "=" not in scheme and not remainder.lstrip().startswith("="): | |
| break |
| if scheme.lower() == "bearer" and separator: | ||
| collecting = True | ||
| auth_params = [remainder.strip()] | ||
| continue |
There was a problem hiding this comment.
P2: When a WWW-Authenticate header contains more than one Bearer challenge, the parser currently keeps overwriting the collected auth params and ends up using the last Bearer challenge. That can make scope/resource metadata extraction come from a different challenge than intended. The overwrite happens because every Bearer ... segment reinitializes auth_params; stopping at the first Bearer challenge (or only initializing once) would keep behavior consistent with the function contract.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/mcp/client/auth/utils.py, line 50:
<comment>When a `WWW-Authenticate` header contains more than one Bearer challenge, the parser currently keeps overwriting the collected auth params and ends up using the last Bearer challenge. That can make scope/resource metadata extraction come from a different challenge than intended. The overwrite happens because every `Bearer ...` segment reinitializes `auth_params`; stopping at the first Bearer challenge (or only initializing once) would keep behavior consistent with the function contract.</comment>
<file context>
@@ -16,6 +16,52 @@
+
+ for segment in segments:
+ scheme, separator, remainder = segment.partition(" ")
+ if scheme.lower() == "bearer" and separator:
+ collecting = True
+ auth_params = [remainder.strip()]
</file context>
| if scheme.lower() == "bearer" and separator: | |
| collecting = True | |
| auth_params = [remainder.strip()] | |
| continue | |
| if scheme.lower() == "bearer" and separator: | |
| if collecting: | |
| break | |
| collecting = True | |
| auth_params = [remainder.strip()] | |
| continue |
Fixes #3009.
extract_field_from_www_authcurrently matchesfield_nameas a substring of another auth-param name, so fields likescopecan be shadowed byerror_scopeandresource_metadatacan be shadowed byx_resource_metadata.This change anchors the auth-param name to the start of the header or a parameter separator, and adds regressions for both the shadowing and false-positive cases.
Tested with:
PYTHONPATH=src:src/mcp-types python -m pytest -q tests/client/test_auth.py -k "extract_field_from_www_auth_valid_cases or extract_field_from_www_auth_invalid_cases"PYTHONPATH=src:src/mcp-types python -m pytest -q tests/client/test_scope_bug_1630.pyPYTHONPATH=src:src/mcp-types python -m pytest -q tests/client/test_auth.py