Skip to content

Commit bc629bd

Browse files
committed
fix(mcp): enhance CRLF injection protection in header handling
1 parent d3ef532 commit bc629bd

3 files changed

Lines changed: 42 additions & 22 deletions

File tree

src/google/adk/tools/mcp_tool/_internal.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,9 @@ def validate_header_format(header_format: str) -> None:
152152
"""
153153
if "\r" in header_format or "\n" in header_format:
154154
raise ValueError(
155-
"Header format string cannot contain CRLF (carriage return or line feed) "
156-
"characters due to header injection risk. "
157-
f"Invalid format: {repr(header_format)}"
155+
"Header format string cannot contain CRLF (carriage return or line"
156+
" feed) characters due to header injection risk. Invalid format:"
157+
f" {repr(header_format)}"
158158
)
159159

160160

src/google/adk/tools/mcp_tool/mcp_toolset.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@
6363

6464
logger = logging.getLogger("google_adk." + __name__)
6565

66+
T = TypeVar("T")
67+
6668

6769
def create_session_state_header_provider(
6870
state_key: str,
@@ -94,7 +96,8 @@ def create_session_state_header_provider(
9496
**Security Best Practices:**
9597
9698
1. **Token Security**: Use ``request_state`` for sensitive, short-lived tokens
97-
(JWTs, API keys) instead of ``session.state`` to avoid persisting sensitive data.
99+
(JWTs, API keys) instead of ``session.state`` to avoid persisting
100+
sensitive data.
98101
99102
2. **Header Validation**: Header names and values are automatically validated
100103
according to RFC 7230 to prevent injection attacks.
@@ -141,7 +144,9 @@ def create_session_state_header_provider(
141144
response = await agent.run(
142145
RunAgentRequest(
143146
session_id="user-123",
144-
request_state={"jwt_token": "eyJhbG..."} # Ephemeral, not persisted
147+
request_state={ # Ephemeral, not persisted
148+
"jwt_token": "eyJhbG..."
149+
}
145150
)
146151
)
147152
"""
@@ -157,6 +162,10 @@ def provider(ctx: ReadonlyContext) -> Dict[str, str]:
157162

158163
validate_header_value(state_key, value, strict=strict)
159164
formatted_value = header_format.format(value=value)
165+
# Strip CRLF from the interpolated value to prevent header injection.
166+
# The format string is validated at construction time, but the runtime
167+
# value comes from session state and must never contain CRLF here.
168+
formatted_value = formatted_value.replace("\r", "").replace("\n", "")
160169
sanitized_value = sanitize_header_value(formatted_value)
161170

162171
return {header_name: sanitized_value}

tests/unittests/tools/mcp_tool/test_jwt_token_propagation.py

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -549,24 +549,35 @@ def test_header_value_sanitization_rfc_compliant(self):
549549
result = sanitize_header_value(input_val)
550550
assert result == expected
551551

552-
def test_header_value_preserves_rfc_folding(self):
553-
"""Test that legitimate CRLF sequences for header folding are preserved."""
554-
from google.adk.tools.mcp_tool._internal import sanitize_header_value
552+
def test_header_value_crlf_stripped_in_provider(self):
553+
"""Test that CRLF in state values is stripped to prevent header injection."""
554+
from google.adk.tools.mcp_tool.mcp_toolset import create_session_state_header_provider
555555

556-
# Multi-line headers with proper folding should be preserved (RFC 7230 §3.2.4)
557-
folding_cases = [
558-
("Authorization: Bearer token", "Authorization: Bearer token"),
559-
("X-Custom: value1\r\n\tvalue2", "X-Custom: value1\r\n\tvalue2"),
560-
("X-Header: line1\r\n line2", "X-Header: line1\r\n line2"),
561-
(
562-
"Content-Type: application/json\r\n\tcharset=utf-8",
563-
"Content-Type: application/json\r\n\tcharset=utf-8",
564-
),
556+
# CRLF in the token value should be stripped from the final header
557+
malicious_cases = [
558+
("tok\r\nX-Injected: evil", "Bearer tokX-Injected: evil"),
559+
("tok\nInjected: bad", "Bearer tokInjected: bad"),
560+
("tok\rAnother: header", "Bearer tokAnother: header"),
561+
("tok\r\n", "Bearer tok"),
562+
("tok\n", "Bearer tok"),
563+
("tok\r", "Bearer tok"),
565564
]
566565

567-
for folding_case in folding_cases:
568-
result = sanitize_header_value(folding_case[0])
569-
assert result == folding_case[1]
566+
for malicious_value, expected_header in malicious_cases:
567+
mock_context = Mock(spec=ReadonlyContext)
568+
mock_context.state = {"jwt_token": malicious_value}
569+
provider = create_session_state_header_provider(state_key="jwt_token")
570+
headers = provider(mock_context)
571+
assert headers == {
572+
"Authorization": expected_header
573+
}, f"Failed for value {malicious_value!r}"
574+
575+
# Clean values should be unchanged
576+
mock_context = Mock(spec=ReadonlyContext)
577+
mock_context.state = {"jwt_token": "clean-token-123"}
578+
provider = create_session_state_header_provider(state_key="jwt_token")
579+
headers = provider(mock_context)
580+
assert headers == {"Authorization": "Bearer clean-token-123"}
570581

571582
def test_header_value_validation_rfc_compliant(self):
572583
"""Test that header value validation is RFC 7230 compliant."""
@@ -659,8 +670,8 @@ def test_binary_data_handling(self):
659670

660671
def test_header_format_crlf_injection_protection(self):
661672
"""Test that header format strings with CRLF sequences are rejected."""
662-
from google.adk.tools.mcp_tool.mcp_toolset import create_session_state_header_provider
663673
from google.adk.tools.mcp_tool._internal import validate_header_format
674+
from google.adk.tools.mcp_tool.mcp_toolset import create_session_state_header_provider
664675

665676
# Valid format strings should be accepted
666677
valid_formats = [
@@ -691,5 +702,5 @@ def test_header_format_crlf_injection_protection(self):
691702
create_session_state_header_provider(
692703
state_key="token",
693704
header_name="Authorization",
694-
header_format="Bearer {value}\r\nX-Injected: evil"
705+
header_format="Bearer {value}\r\nX-Injected: evil",
695706
)

0 commit comments

Comments
 (0)