Skip to content

Commit 5bd9b7a

Browse files
authored
Merge pull request #671 from koic/scrub_the_header_values_echoed_into_error_responses
Scrub the header and body values echoed into error responses
2 parents 5cae935 + e872b1e commit 5bd9b7a

2 files changed

Lines changed: 99 additions & 6 deletions

File tree

‎lib/mcp/server/transports/streamable_http_transport.rb‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -905,7 +905,7 @@ def handle_modern(request, header_version, body_string: nil)
905905
message: "Unsupported protocol version",
906906
data: {
907907
supported: MCP::Configuration::SUPPORTED_MODERN_PROTOCOL_VERSIONS,
908-
requested: header_version,
908+
requested: printable_value(header_version),
909909
},
910910
id: body[:id],
911911
)
@@ -990,7 +990,7 @@ def validate_modern_headers(request, body, header_version)
990990
meta_version = params.is_a?(Hash) && params[:_meta].is_a?(Hash) ? params.dig(:_meta, :"io.modelcontextprotocol/protocolVersion") : nil
991991
if meta_version && meta_version != header_version
992992
return header_mismatch_response(
993-
"MCP-Protocol-Version header value '#{header_version}' does not match body value '#{meta_version}'",
993+
"MCP-Protocol-Version header value '#{printable_value(header_version)}' does not match body value '#{printable_value(meta_version)}'",
994994
body[:id],
995995
)
996996
end
@@ -1009,7 +1009,7 @@ def validate_modern_headers(request, body, header_version)
10091009
end
10101010
if method_header != body[:method]
10111011
return header_mismatch_response(
1012-
"Mcp-Method header value '#{method_header}' does not match body value '#{body[:method]}'",
1012+
"Mcp-Method header value '#{printable_value(method_header)}' does not match body value '#{printable_value(body[:method])}'",
10131013
body[:id],
10141014
)
10151015
end
@@ -1020,15 +1020,15 @@ def validate_modern_headers(request, body, header_version)
10201020
if body_name
10211021
if name_header.to_s.empty?
10221022
return header_mismatch_response(
1023-
"Mcp-Name header is required for `#{body[:method]}`",
1023+
"Mcp-Name header is required for `#{printable_value(body[:method])}`",
10241024
body[:id],
10251025
)
10261026
end
10271027

10281028
decoded_name = decode_header_value(name_header)
10291029
if decoded_name != body_name
10301030
return header_mismatch_response(
1031-
"Mcp-Name header value '#{decoded_name}' does not match body value '#{body_name}'",
1031+
"Mcp-Name header value '#{printable_value(decoded_name)}' does not match body value '#{printable_value(body_name)}'",
10321032
body[:id],
10331033
)
10341034
end
@@ -1611,6 +1611,15 @@ def write_listen_closing_result(subscription)
16111611
end
16121612
end
16131613

1614+
# A client-supplied value for an error message or `data` member. A header value reaches the transport as
1615+
# ASCII-8BIT from a Rack server, or tagged UTF-8 from a test request or a framework, and a body value comes
1616+
# out of the JSON parser; a value holding bytes that are not valid UTF-8 would make the response's `to_json`
1617+
# raise and turn the intended 400 into a 500 with an exception report. Such bytes are replaced with U+FFFD;
1618+
# a valid value comes through unchanged.
1619+
def printable_value(value)
1620+
value.to_s.dup.force_encoding(Encoding::UTF_8).scrub
1621+
end
1622+
16141623
def header_mismatch_response(message, id)
16151624
json_rpc_error_response(
16161625
status: 400,
@@ -2095,7 +2104,7 @@ def validate_protocol_version_header(request)
20952104
json_rpc_error_response(
20962105
status: 400,
20972106
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
2098-
message: "Bad Request: Unsupported protocol version: #{header_value}. Supported versions: #{supported}",
2107+
message: "Bad Request: Unsupported protocol version: #{printable_value(header_value)}. Supported versions: #{supported}",
20992108
)
21002109
end
21012110

‎test/mcp/server/transports/streamable_http_transport_test.rb‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6327,6 +6327,90 @@ def string
63276327
assert_equal "2027-01-01", body.dig("error", "data", "requested")
63286328
end
63296329

6330+
test "modern POST answers 400 with -32022 when the requested version is not valid UTF-8" do
6331+
# The echoed `requested` member is scrubbed, where `to_json` on the raw value raised and the rescue answered 500 with an exception report.
6332+
reported = []
6333+
original_reporter = MCP.configuration.exception_reporter
6334+
MCP.configuration.exception_reporter = ->(error, _context) { reported << error }
6335+
version = "2026-07-28\xFF".dup.force_encoding(Encoding::UTF_8)
6336+
6337+
response = @transport.handle_request(modern_rack_request(modern_body("ping", {}), version: version))
6338+
6339+
assert_equal 400, response[0]
6340+
body = JSON.parse(response[2][0])
6341+
assert_equal(-32022, body.dig("error", "code"))
6342+
assert_equal "2026-07-28\uFFFD", body.dig("error", "data", "requested")
6343+
assert_empty reported
6344+
ensure
6345+
MCP.configuration.exception_reporter = original_reporter
6346+
end
6347+
6348+
test "modern POST answers 400 with -32020 when a mirror header that mismatches is not valid UTF-8" do
6349+
reported = []
6350+
original_reporter = MCP.configuration.exception_reporter
6351+
MCP.configuration.exception_reporter = ->(error, _context) { reported << error }
6352+
invalid_method = "tools/call\xFF".dup.force_encoding(Encoding::UTF_8)
6353+
invalid_name = "=?base64?#{["file:///\xFF".b].pack("m0")}?="
6354+
6355+
by_method = @transport.handle_request(modern_rack_request(
6356+
modern_body("ping", {}),
6357+
headers: { "HTTP_MCP_METHOD" => invalid_method },
6358+
))
6359+
by_name = @transport.handle_request(modern_rack_request(
6360+
modern_body("resources/read", { uri: "file:///a.txt" }),
6361+
headers: { "HTTP_MCP_NAME" => invalid_name },
6362+
))
6363+
6364+
[by_method, by_name].each do |response|
6365+
assert_equal 400, response[0]
6366+
body = JSON.parse(response[2][0])
6367+
assert_equal(-32020, body.dig("error", "code"))
6368+
assert_includes body.dig("error", "message"), "\uFFFD"
6369+
end
6370+
assert_empty reported
6371+
ensure
6372+
MCP.configuration.exception_reporter = original_reporter
6373+
end
6374+
6375+
test "modern POST answers 400 with -32020 when the body's envelope version is not valid UTF-8" do
6376+
# The body side of the mismatch message comes out of the JSON parser, which passes such bytes through.
6377+
reported = []
6378+
original_reporter = MCP.configuration.exception_reporter
6379+
MCP.configuration.exception_reporter = ->(error, _context) { reported << error }
6380+
body_string = modern_body("ping", {}, version: "2026-07-28X").b.sub("2026-07-28X", "2026-07-28\xFF".b)
6381+
6382+
response = @transport.handle_request(modern_rack_request(body_string))
6383+
6384+
assert_equal 400, response[0]
6385+
body = JSON.parse(response[2][0])
6386+
assert_equal(-32020, body.dig("error", "code"))
6387+
assert_includes body.dig("error", "message"), "\uFFFD"
6388+
assert_empty reported
6389+
ensure
6390+
MCP.configuration.exception_reporter = original_reporter
6391+
end
6392+
6393+
test "modern POST echoes a valid non-ASCII mirror header given as bytes in its -32020 message" do
6394+
# A Rack server hands the header over as ASCII-8BIT; json 3.1 refuses to serialize such a string
6395+
# when it holds bytes above 0x7F, valid UTF-8 or not, so the mismatch answered 500 there.
6396+
reported = []
6397+
original_reporter = MCP.configuration.exception_reporter
6398+
MCP.configuration.exception_reporter = ->(error, _context) { reported << error }
6399+
6400+
response = @transport.handle_request(modern_rack_request(
6401+
modern_body("ping", {}),
6402+
headers: { "HTTP_MCP_METHOD" => "p\xC3\xAFng".b },
6403+
))
6404+
6405+
assert_equal 400, response[0]
6406+
body = JSON.parse(response[2][0])
6407+
assert_equal(-32020, body.dig("error", "code"))
6408+
assert_includes body.dig("error", "message"), "p\u00EFng"
6409+
assert_empty reported
6410+
ensure
6411+
MCP.configuration.exception_reporter = original_reporter
6412+
end
6413+
63306414
test "modern POST with a header/body version mismatch returns 400 with -32020" do
63316415
# Header names the supported modern version; the body envelope claims another.
63326416
response = @transport.handle_request(modern_rack_request(

0 commit comments

Comments
 (0)