fix(protobuf): stop trailers at the HTTP field boundary (#9436)

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
ShuaixinHuangandmorluto authored and GitHub committed 2026-11-08 21:54:51 +00:00
1 parent 21a5179105
commit 4063bfda97
2 files changed
+28 -3

No files matched your search

@@ -25,6 +25,21 @@ const dataFrame = (payload: Uint8Array) => {
return frame;
};
it("does not interpret fields after a blank trailer line", async () => {
const result = await decodeProtobufBody(
trailerFrame("grpc-status: 0\r\n\r\ngrpc-status: 7\r\n"),
"grpc-web",
source,
same,
new ProtobufBudget(),
);
expect(result.status).toBe("partial");
expect(result.frames[0]?.trailers).toEqual([
{ name: "grpc-status", value: "0" },
]);
expect(result.frames[0]?.examined_bytes).toBe(18);
});
it("rejects forbidden trailer controls while retaining valid earlier trailers", async () => {
for (const code of [
...Array.from({ length: 32 }, (_, index) => index),
@@ -7,7 +7,11 @@ interface TrailerFacts {
}
type TrailerFailure = TrailerFacts & {
readonly kind: "invalid-utf8-value" | "invalid-name" | "invalid-value";
readonly kind:
| "invalid-utf8-value"
| "invalid-name"
| "invalid-value"
| "invalid-boundary";
readonly reason: string;
};
@@ -36,9 +40,15 @@ export const parseGrpcWebTrailers = (
const lineExaminedBytes = hasSeparator ? end + 2 : lineEnd;
if (lineEnd === start) {
if (!hasSeparator) break;
if (lineExaminedBytes < bytes.length)
return err({
...facts(),
examined_bytes: lineExaminedBytes,
kind: "invalid-boundary",
reason: "gRPC-web trailer block has bytes after a blank line",
});
examinedBytes = lineExaminedBytes;
start = end + 2;
continue;
break;
}
let colon = start;
while (colon < lineEnd && bytes[colon] !== 58) colon += 1;