Skip to content

Client: envelope flag bytes are only ever tested on the protocol that defines them #1120

Description

@iainmcgin

Summary

Every envelope flag value the suite exercises is only ever sent on the protocol that defines it. No case sends a flag byte on a protocol where that flag is reserved, so a client that misinterprets one passes the full suite. In one implementation that gap let a body frame override real HTTP/2 trailers, substituting a fabricated grpc-status for the server's actual one.

The coverage matrix

Extracted from every testCases entry in testsuites/*.yaml that sets an explicit envelope flags value (89 cases in total):

flag protocols it is sent on meaning there
0 Connect, gRPC, gRPC-Web data
1 Connect, gRPC, gRPC-Web compressed data
2 Connect only END_STREAM
3 Connect only compressed END_STREAM
128 gRPC-Web only trailer frame
129 gRPC-Web only compressed trailer frame

The two rows that matter are 2 and 128: each is tested only where it is legitimate. Nothing sends 0x80 on plain gRPC or Connect, and nothing sends 0x02 on gRPC or gRPC-Web, even though those bytes are reserved rather than defined in those protocols. The gRPC framing byte in particular admits only 0x00 and 0x01.

What both reference implementations do

Both reject a 0x80-flagged frame on plain gRPC, so there is no ambiguity about the correct behaviour here — unlike the non-200 question in the sibling issue, this one has a clear answer that simply is not pinned. Measured against connect-go v1.19.1 and grpc-go v1.81.1:

  • connect-go: internal — "protocol error: invalid envelope flags 128"
  • grpc-go: Internal — "grpc: received unexpected payload format 128"

What the gap allowed

connectrpc/connect-rust checked the high bit without gating on the protocol, so on plain gRPC it parsed the frame as a gRPC-Web trailer block. Given a response with a valid data envelope, a 0x80 frame whose payload claimed grpc-status: 5, and real HTTP/2 trailers saying grpc-status: 0, the client returned NotFound with the fabricated message. The genuine trailers were never consulted. A peer able to place bytes in the response body could therefore choose the RPC's terminal status.

All six suites passed throughout.

The same class exists for 0x02. On the streaming decode path an END_STREAM-flagged frame is routed to the Connect end-of-stream parser regardless of protocol, so on plain gRPC a body frame carrying {"error":{"code":"permission_denied",...}} is returned as the stream's terminal error. Connect END_STREAM has no meaning in gRPC, where the status arrives in HTTP/2 trailers.

We have fixed the 0x80 case and are fixing 0x02, but both were found by hand-written interop tests rather than by the suite, which is why we are reporting the gap rather than only the bug.

Suggested cases

RawHTTPResponse.stream already takes per-item flags, so these are test data with no harness change.

For each protocol, one case per flag value the protocol does not define, asserting the client reports a protocol/framing error rather than acting on the frame:

  • gRPC: 0x02, 0x80, 0x81, and ideally an arbitrary reserved bit such as 0x04.
  • gRPC-Web: 0x02 (Connect's END_STREAM has no meaning here).
  • Connect: 0x80.

The highest-value single case is the adversarial one, because it is the one where a permissive client is not merely lenient but wrong: on plain gRPC, a valid data envelope, then a 0x80 frame claiming a non-zero grpc-status, then real HTTP/2 trailers with grpc-status: 0. A conformant client must report the framing error or the real trailers, never the frame's claim.

Happy to send a PR with these.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions