mirror of
https://github.com/morluto/rea.git
synced 2026-11-01 04:16:28 +00:00
fix(mcp): apply the SDK's own Accept and Content-Type checks before a session takeover
Media types are case-insensitive in HTTP, so the takeover guard compared them lower-cased, but the SDK's transport matches Accept as a case-sensitive substring. A header such as Application/JSON passed the guard, closed the live session, and then got 406 from the SDK. The guard now also requires the SDK's Accept substrings and uses its exported isJsonContentType, so it only replaces a session the SDK will accept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
34b11444ba
commit
baff9d359e
2 files changed
+11
-3
No files matched your search
@@ -791,14 +791,16 @@ it("closes the connection when a route read only part of the body and keeps it a
|
||||
}
|
||||
});
|
||||
|
||||
it("does not let a zero-quality or quoted Accept range qualify a session takeover", async () => {
|
||||
it("does not let a zero-quality, quoted or SDK-refused Accept qualify a session takeover", async () => {
|
||||
const { served } = await start();
|
||||
const { client } = await connect(served.url);
|
||||
const url = new URL(served.url);
|
||||
// The second header holds one range: the comma and `text/event-stream` sit inside a quoted parameter.
|
||||
// The third is valid HTTP, but the SDK matches media types with case and would answer 406.
|
||||
for (const accept of [
|
||||
"application/json;q=0, text/event-stream",
|
||||
'application/json;note=", text/event-stream;x=1"',
|
||||
"Application/JSON, Text/Event-Stream",
|
||||
]) {
|
||||
const refused = await rawHttpRequest(url, {
|
||||
method: "POST",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
hostHeaderValidationResponse,
|
||||
isInitializeRequest,
|
||||
isJsonContentType,
|
||||
localhostAllowedHostnames,
|
||||
originValidationResponse,
|
||||
readRequestBody,
|
||||
@@ -145,17 +146,22 @@ const quality = (range: string): number => {
|
||||
};
|
||||
/** The headers the SDK requires of a streamable POST; a request without them is left to the transport's refusal. */
|
||||
const hasStreamablePostHeaders = (request: Request): boolean => {
|
||||
const accept = request.headers.get("accept") ?? "";
|
||||
// Exact media ranges with a positive weight: `application/json-patch+json`, `text/event-streaming`
|
||||
// and `application/json;q=0` do not count.
|
||||
const accepted = new Set(
|
||||
splitUnquoted(request.headers.get("accept") ?? "", ",")
|
||||
splitUnquoted(accept, ",")
|
||||
.filter((range) => quality(range) > 0)
|
||||
.map(mediaType),
|
||||
);
|
||||
// The SDK's own checks too: its Accept test is a case-sensitive substring match, so a header it
|
||||
// would refuse with 406 (such as `Application/JSON`) must not close the live session first.
|
||||
return (
|
||||
accepted.has("application/json") &&
|
||||
accepted.has("text/event-stream") &&
|
||||
mediaType(request.headers.get("content-type") ?? "") === "application/json"
|
||||
accept.includes("application/json") &&
|
||||
accept.includes("text/event-stream") &&
|
||||
isJsonContentType(request.headers.get("content-type"))
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user