[GH-ISSUE #5386] [Bug] gRPC-web trailer control characters are reported as complete decoding #3263

Closed
opened 2026-10-31 00:38:32 +00:00 by bsakaguchi · 2 comments
Owner

Originally created by @xu91102 on GitHub (Oct 20, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/5386

Observed behavior

On source main 074768901f3cdc8205b61773fb1faf6f6c7b3ca8, six actual compiled CLI calls to inspect-web-network-capture <capture.har> har --record 0 --json --full-output completed with exit 0. A valid gRPC-web trailer and five malformed trailers all reported body/frame status: "complete" and counted every byte as examined.

The malformed x-note values contain respectively NUL, bare LF, bare CR, vertical tab, and DEL between a and b. For example x-note: a\x00b\r\n is returned as a successfully decoded trailer value. All six source HAR SHA-256 hashes remained unchanged. These are inert, self-authored HAR files; no recorded URL was fetched or target executed.

Minimal reproduction

Use an ordinary HAR 1.2 response with MIME application/grpc-web+proto, content encoding base64, and this response body:

def frame(payload, flags=0):
    return bytes([flags]) + len(payload).to_bytes(4, "big") + payload
body = frame(bytes.fromhex("0801")) + frame(
    b"grpc-status: 0\r\nx-note: a\x00b\r\n", 128)

Base64-encode body in response.content.text, then run the command above. Replace a\x00b with normal value for the passing control. The field 1 = 1 from the preceding data frame is decoded in every case.

Expected behavior and impact

Retain the original binary sidecar and preceding data facts, but mark a malformed trailer and its body partial using the decoder's existing malformed-header diagnostic. Do not describe the invalid header value as completely examined/decoded. This is a decoding-completeness defect, not an observed network or security incident.

gRPC-web encodes trailers as an HTTP/1 header block. RFC 9110 section 5.5 excludes these control characters from field values. SP/HTAB handling and retained source bytes should remain unchanged.

Environment and limits

REA source/package metadata 6.7.0; pinned Node 24.18.0; macOS arm64. Only the six compiled CLI controls above ran. Published package, MCP transport, live gRPC servers, Windows/Linux and performance: NOT RUN. Source-derived cause: ProtobufBody.ts validates trailer names but not the field-value characters.

Implemented and validated on the fork

The existing shared trailer owner now rejects HTTP-forbidden control characters via its existing partial-result path. SP/HTAB and empty values remain accepted. Original body bytes, earlier decoded protobuf fields, and earlier valid trailers remain retained; framing/UTF-8/redaction/budget behavior and the public schemas are unchanged. Normal hooks, commit and fork push produced 64963f10e5e57b6a48c06875d6f82c625c933c62 on xu91102/rea:codex/grpc-web-trailer-values, based on 074768901. Frozen bytes match before/after commit/push; separate independent Standards/Spec reviews pass.

Final source validation: four focused files/32 PASS plus five fresh README tests = five distinct files/37 PASS, zero skip; check/docs/discovery pass (1,720 test files exactly once). The normal local installed-package verifier and explicit pack dry-run pass, with the official MCP client discovering 209 tools/six prompts. A new feature tarball has 1,905 matching installed files. The same six immutable baseline inputs pass in both compiled and installed CLI runs (five malformed partial/one valid complete); a real installed stdio MCP session also verifies valid/partial/retained results and session health in four calls. No live server, agent/model, Windows/Linux or performance qualification is claimed.

No upstream PR or upstream CI exists for this change yet. No repeat create request was made while the prior creation refusal has no confirmed recovery. This issue remains open; no issue-close action or reply to another author was sent.

Publication and overlap follow-up

The ordinary Draft request on 2026-10-20 was denied by GitHub. A later authenticated compare-page check established the repository's concurrent limit of three open PRs for users without write access; this supersedes the earlier unknown-policy diagnosis.

A fresh duplicate check then found PR #5422 by iamdgarcia (head fa268ea13ea738084b86d180a9d7a064673f2f8e). I read its complete actual two-file production/test diff. It validates the same forbidden trailer field-value controls at the same shared ProtobufBody owner, via a character-code loop rather than this fork's character class. It is still OPEN and unmerged at this check. I am keeping my fork branch read-only and am not submitting another same-root PR or packaging the remaining text-framing/CLI/MCP/docs coverage as a new contribution.

I have not run that foreign head, and its description of an independent implementation does not establish adoption of this fork source or author credit. My original reproduction and local validation remain as recorded above, bound to base 074768901 and fork head 64963f1; there is no upstream PR or upstream CI for my #5386 implementation. This issue remains open; no issue-close action or reply to another author was sent.

Mainline resolution follow-up

The earlier overlap check found #5422 open. It was subsequently closed UNMERGED by morluto at 2026-10-20T11:40:20Z. Main commit 9cf7a7cb0, authored by morluto, explicitly references this issue and #5422 and fixes the same forbidden-field-value root in a shared typed trailer parser. I read the complete actual five-file main patch. This is mainline coverage, not an upstream PR or a confirmed adoption/coauthor credit for my fork patch. The original fork remains read-only; I will not package remaining text-framing/CLI/MCP/docs scope as a separate same-root contribution.

I then normally fast-forwarded a separate clean discovery checkout and built captured main ee12d517a25d34853d40e31fc039c7d1576c60e1. All six original immutable HAR samples were run through its actual compiled public CLI: five malformed controls now return partial and the valid control remains complete, all exits0. Original file SHA-256, retained binary sidecars, prior protobuf fields and the earlier valid trailer are unchanged. These are six fresh compiled-CLI checks on that exact main, not a full-main, installed-package, MCP, live-server, Windows/Linux or performance qualification. No foreign branch runtime or previous fork test totals are borrowed. The issue state was left unchanged; no issue-close or foreign-reply call was made.

Originally created by @xu91102 on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5386 ### Observed behavior On source main `074768901f3cdc8205b61773fb1faf6f6c7b3ca8`, six actual compiled CLI calls to `inspect-web-network-capture <capture.har> har --record 0 --json --full-output` completed with exit 0. A valid gRPC-web trailer and five malformed trailers all reported body/frame `status: "complete"` and counted every byte as examined. The malformed `x-note` values contain respectively NUL, bare LF, bare CR, vertical tab, and DEL between `a` and `b`. For example `x-note: a\x00b\r\n` is returned as a successfully decoded trailer value. All six source HAR SHA-256 hashes remained unchanged. These are inert, self-authored HAR files; no recorded URL was fetched or target executed. ### Minimal reproduction Use an ordinary HAR 1.2 response with MIME `application/grpc-web+proto`, content encoding `base64`, and this response body: ```python def frame(payload, flags=0): return bytes([flags]) + len(payload).to_bytes(4, "big") + payload body = frame(bytes.fromhex("0801")) + frame( b"grpc-status: 0\r\nx-note: a\x00b\r\n", 128) ``` Base64-encode `body` in `response.content.text`, then run the command above. Replace `a\x00b` with `normal value` for the passing control. The field `1 = 1` from the preceding data frame is decoded in every case. ### Expected behavior and impact Retain the original binary sidecar and preceding data facts, but mark a malformed trailer and its body partial using the decoder's existing malformed-header diagnostic. Do not describe the invalid header value as completely examined/decoded. This is a decoding-completeness defect, not an observed network or security incident. [gRPC-web](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md#protocol-differences-vs-grpc-over-http2) encodes trailers as an HTTP/1 header block. [RFC 9110 section 5.5](https://www.rfc-editor.org/rfc/rfc9110.html#section-5.5) excludes these control characters from field values. SP/HTAB handling and retained source bytes should remain unchanged. ### Environment and limits REA source/package metadata 6.7.0; pinned Node 24.18.0; macOS arm64. Only the six compiled CLI controls above ran. Published package, MCP transport, live gRPC servers, Windows/Linux and performance: NOT RUN. Source-derived cause: `ProtobufBody.ts` validates trailer names but not the field-value characters. ### Implemented and validated on the fork The existing shared trailer owner now rejects HTTP-forbidden control characters via its existing partial-result path. SP/HTAB and empty values remain accepted. Original body bytes, earlier decoded protobuf fields, and earlier valid trailers remain retained; framing/UTF-8/redaction/budget behavior and the public schemas are unchanged. Normal hooks, commit and fork push produced `64963f10e5e57b6a48c06875d6f82c625c933c62` on `xu91102/rea:codex/grpc-web-trailer-values`, based on `074768901`. Frozen bytes match before/after commit/push; separate independent Standards/Spec reviews pass. Final source validation: four focused files/32 PASS plus five fresh README tests = five distinct files/37 PASS, zero skip; check/docs/discovery pass (1,720 test files exactly once). The normal local installed-package verifier and explicit pack dry-run pass, with the official MCP client discovering 209 tools/six prompts. A new feature tarball has 1,905 matching installed files. The same six immutable baseline inputs pass in both compiled and installed CLI runs (five malformed partial/one valid complete); a real installed stdio MCP session also verifies valid/partial/retained results and session health in four calls. No live server, agent/model, Windows/Linux or performance qualification is claimed. No upstream PR or upstream CI exists for this change yet. No repeat create request was made while the prior creation refusal has no confirmed recovery. This issue remains open; no issue-close action or reply to another author was sent. ### Publication and overlap follow-up The ordinary Draft request on 2026-10-20 was denied by GitHub. A later authenticated compare-page check established the repository's concurrent limit of three open PRs for users without write access; this supersedes the earlier unknown-policy diagnosis. A fresh duplicate check then found PR #5422 by iamdgarcia (head `fa268ea13ea738084b86d180a9d7a064673f2f8e`). I read its complete actual two-file production/test diff. It validates the same forbidden trailer field-value controls at the same shared `ProtobufBody` owner, via a character-code loop rather than this fork's character class. It is still OPEN and unmerged at this check. I am keeping my fork branch read-only and am not submitting another same-root PR or packaging the remaining text-framing/CLI/MCP/docs coverage as a new contribution. I have not run that foreign head, and its description of an independent implementation does not establish adoption of this fork source or author credit. My original reproduction and local validation remain as recorded above, bound to base `074768901` and fork head `64963f1`; there is no upstream PR or upstream CI for my #5386 implementation. This issue remains open; no issue-close action or reply to another author was sent. ### Mainline resolution follow-up The earlier overlap check found #5422 open. It was subsequently closed UNMERGED by morluto at 2026-10-20T11:40:20Z. Main commit [`9cf7a7cb0`](https://github.com/morluto/rea/commit/9cf7a7cb0d3c72a1121fde280cb11bf911af8055), authored by morluto, explicitly references this issue and #5422 and fixes the same forbidden-field-value root in a shared typed trailer parser. I read the complete actual five-file main patch. This is mainline coverage, not an upstream PR or a confirmed adoption/coauthor credit for my fork patch. The original fork remains read-only; I will not package remaining text-framing/CLI/MCP/docs scope as a separate same-root contribution. I then normally fast-forwarded a separate clean discovery checkout and built captured main `ee12d517a25d34853d40e31fc039c7d1576c60e1`. All six original immutable HAR samples were run through its actual compiled public CLI: five malformed controls now return partial and the valid control remains complete, all exits0. Original file SHA-256, retained binary sidecars, prior protobuf fields and the earlier valid trailer are unchanged. These are six fresh compiled-CLI checks on that exact main, not a full-main, installed-package, MCP, live-server, Windows/Linux or performance qualification. No foreign branch runtime or previous fork test totals are borrowed. The issue state was left unchanged; no issue-close or foreign-reply call was made.
Author
Owner

@CassieFox23 commented on GitHub (Oct 20, 2026):

TL;DR: This looks fixed on main. A gRPC-web trailer with a control character is now marked partial, not complete. Closing is the maintainers' call.

I checked main 1629266e3 (Linux, Node 24.18.0). The fix is 9cf7a7cb0.

I wrote six small HARs. Each has a data frame, then a trailer with x-note: a?b. The ? was NUL, LF, CR, VT or DEL. The sixth was a normal value as a control. All six runs of inspect-web-network-capture exited 0.

x-note value Coverage Trailer frame Data frame and field 1
normal value complete complete complete, field 1 = 1
NUL, LF, CR, VT, DEL (5 runs) partial partial complete, field 1 = 1

Each bad trailer gets one reason. It names a forbidden control character in the value. The earlier grpc-status: 0 is kept. The original bytes stay in the binary sidecar.

Not run

  • The published package, MCP, and live gRPC servers.
  • Tab, space, and empty values.

Thanks, xu91102, for the clear report.

Commands
rea inspect-web-network-capture nul.har har --record 0 --json --full-output

Exit 0. Trailer frame: "status":"partial","error":{"offset":12,"reason":"gRPC-web trailer field value contains a forbidden control character"},"trailers":[{"name":"grpc-status","value":"0"}]. Body: "status":"partial". Coverage: {"status":"partial","selected_records":1,"inspected_records":1}.

Test setup
  • Box: Nobara 44 (Fedora 44 base), Linux 7.2.11
  • CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads
  • RAM: 32 GB DDR4-3600
  • Node: 24.18.0
  • Head: 1629266e3

🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊

<!-- gh-comment-id:6299401387 --> @CassieFox23 commented on GitHub (Oct 20, 2026): **TL;DR:** This looks fixed on main. A gRPC-web trailer with a control character is now marked partial, not complete. Closing is the maintainers' call. I checked main 1629266e3 (Linux, Node 24.18.0). The fix is 9cf7a7cb0. I wrote six small HARs. Each has a data frame, then a trailer with `x-note: a?b`. The `?` was NUL, LF, CR, VT or DEL. The sixth was a normal value as a control. All six runs of `inspect-web-network-capture` exited 0. | x-note value | Coverage | Trailer frame | Data frame and field 1 | | --- | --- | --- | --- | | normal value | complete | complete | complete, field 1 = 1 | | NUL, LF, CR, VT, DEL (5 runs) | partial | partial | complete, field 1 = 1 | Each bad trailer gets one reason. It names a forbidden control character in the value. The earlier `grpc-status: 0` is kept. The original bytes stay in the binary sidecar. ### Not run - The published package, MCP, and live gRPC servers. - Tab, space, and empty values. Thanks, xu91102, for the clear report. <details> <summary>Commands</summary> ```sh rea inspect-web-network-capture nul.har har --record 0 --json --full-output ``` Exit 0. Trailer frame: `"status":"partial","error":{"offset":12,"reason":"gRPC-web trailer field value contains a forbidden control character"},"trailers":[{"name":"grpc-status","value":"0"}]`. Body: `"status":"partial"`. Coverage: `{"status":"partial","selected_records":1,"inspected_records":1}`. </details> <details> <summary>Test setup</summary> - Box: Nobara 44 (Fedora 44 base), Linux 7.2.11 - CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads - RAM: 32 GB DDR4-3600 - Node: 24.18.0 - Head: 1629266e3 </details> 🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊
Author
Owner

@morluto commented on GitHub (Oct 21, 2026):

Closing as fixed on main by github.com/morluto/rea@9cf7a7cb0d, retained at 67a6477f01.

The gRPC-web trailer owner rejects forbidden C0/DEL characters while preserving allowed HTAB. Producer regressions check partial decoding, prior valid facts and raw bytes, plus valid HTAB/visible text controls. The issue's later main check also records the original malformed HAR controls returning partial results.

This closure follows current-main producer and regression source inspection and the existing issue evidence. Tests and runtime reproductions were not rerun in this audit; no additional platform/provider coverage is implied.

<!-- gh-comment-id:6324292491 --> @morluto commented on GitHub (Oct 21, 2026): Closing as fixed on main by https://github.com/morluto/rea/commit/9cf7a7cb0d3c72a1121fde280cb11bf911af8055, retained at 67a6477f011ae358fc333e5a6e26c86d932654ed. The gRPC-web trailer owner rejects forbidden C0/DEL characters while preserving allowed HTAB. Producer regressions check partial decoding, prior valid facts and raw bytes, plus valid HTAB/visible text controls. The issue's later main check also records the original malformed HAR controls returning partial results. This closure follows current-main producer and regression source inspection and the existing issue evidence. Tests and runtime reproductions were not rerun in this audit; no additional platform/provider coverage is implied.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3263
No description provided.