Abstract byte stream passing through two offset parser windows that disagree on HTTP request boundaries.

The Proxy Saw One Request: http4s Ember HTTP Desync

400 Bad Request appeared at the edge. The origin logged GET /lab-marker anyway.

That pair of events is the useful clue. It says the two HTTP parsers did not merely disagree about validity. They disagreed about where one request ended and the next began. In a pooled backend connection, the bytes left over from that argument become the next security decision.

The scene is a controlled model, not a reported intrusion. It frames three http4s Ember advisories published on September 15, 2026: CVE-2026-69204, CVE-2026-69205, and CVE-2026-69216.[1][2][3] Together they cover mixed framing headers, Transfer-Encoding token interpretation, and lenient chunk boundaries. The fixes shipped in http4s 0.23.35 and 1.0.0-M47.[4]

Tuesday’s RouterOS analysis followed identity data as it crossed into command syntax. This time the boundary is between two machines that both speak HTTP correctly enough to serve traffic, but not identically enough to share a byte stream safely.

The attack surface is the parser pair

Request smuggling is often described as a malformed-request problem. That description hides the operational condition that matters: a front end and an origin consume the same persistent connection using different message-length rules.

The attacker is not trying to make one parser crash. The operator sends an ambiguous message, observes timing or response-order behavior, then asks a narrower question: which component trusts Content-Length, which trusts Transfer-Encoding, and which bytes survive for the next parse cycle?

RFC 9112 defines the precedence rules. A server that receives both Transfer-Encoding and Content-Length must treat the combination as an error because it can indicate request smuggling. Transfer-coding names are case-insensitive. A chunk size is hexadecimal digits, not a signed integer with whitespace tolerated around it.[5]

Those rules are useful because they remove interpretation, not because they make every individual parser elegant. Desynchronization needs two valid opinions. Strict rejection collapses the state space to one.

Three differentials, one backend socket

CVE-2026-69204 is the familiar CL.TE shape. Historical Ember accepted a request containing both framing headers and selected chunked decoding, while an intermediary could frame the same body by Content-Length or strip one header during forwarding. The project classified the issue as critical. Its advisory calls out front-end ACL bypass, cross-user request hijacking, and cache poisoning when the origin sits behind a keep-alive intermediary that forwards the ambiguity.[1]

CVE-2026-69205 is subtler. Ember used a case-sensitive substring test equivalent to:

hValue.contains("chunked")

Transfer-Encoding: Chunked therefore failed the test and fell back to Content-Length, even though an RFC-compliant intermediary recognizes the coding case-insensitively. The inverse also existed: notchunked contained the expected lowercase substring and could be accepted as chunked by Ember while a compliant peer rejected it. The patch replaced substring matching with exact, case-insensitive token checks and pinned header-byte decoding to ISO-8859-1.[2][6]

That charset change closes an especially instructive edge case. If raw header bytes become Unicode before framing checks, Unicode case folding can create equivalence that does not exist on the wire. The disclosed example used the Kelvin sign, which folds toward k in a case-insensitive string comparison. Framing logic should operate on a deliberately narrow byte grammar, not inherit a platform text decoder’s generosity.[2]

CVE-2026-69216 moved the differential into chunk parsing. The decoder trimmed the chunk-size token and delegated to a signed numeric parser. Values with a leading +, -, or surrounding whitespace could therefore receive a different interpretation from a stricter intermediary. It also failed to enforce exactly one CRLF after chunk data. The fix now requires 1*HEXDIG and the mandatory trailing CRLF before advancing.[3][7]

Diagram comparing a strict intermediary with historical http4s Ember parsing of the same HTTP byte stream.
The same schematic bytes can be rejected at one hop and framed differently at another. Detection requires correlation across the upstream connection.

These are not three spellings of the same bug. They occupy three stages:

  1. Select the body framing mode.
  2. Interpret the transfer-coding token.
  3. Find boundaries inside the chunked body.

A gateway team that blocks only duplicate length indicators still leaves token normalization and chunk grammar untested.

A safe differential lab

The following inert request uses a reserved .invalid host and a harmless marker path. It should be exercised only inside an authorized parser harness, never against an external service.

POST /public HTTP/1.1
Host: lab.invalid
Transfer-Encoding: Chunked
Content-Length: 38

0

GET /lab-marker HTTP/1.1
X: y

An in-memory Python model executed for this article compared a strict edge view with the historical Ember conditions disclosed by the project. It opened no socket and sent no network traffic. All three cases, mixed headers, mixed-case transfer coding, and substring transfer coding, produced a framing differential.

The important test output is not “request accepted.” Record both parser views:

{
  "edge":  {"accepted": false, "body_mode": "none"},
  "origin": {"accepted": true,  "body_mode": "chunked"},
  "differential": true
}

For a real pre-production stack, replace the model with a two-sided harness that captures what the ingress accepted, the exact normalized bytes forwarded, the backend connection identity, and what the origin parsed. Keep the marker endpoint non-privileged and isolate the environment. A single HTTP status at the client cannot tell you which component consumed which bytes.

This is where generic WAF-bypass testing is insufficient. The test target is not one rule engine. It is the composition of proxy normalization, connection reuse, and origin parsing.

The operator’s decision tree

A disciplined assessment starts with architecture, not payload mutation.

First, establish whether HTTP/1.1 reaches Ember after the edge. HTTP/2 on the client side does not remove risk if the gateway downgrades to a pooled HTTP/1.1 origin connection. Then determine whether requests are buffered and re-encoded or streamed largely intact. Finally, identify whether backend keep-alive allows residue from one client request to influence the next.

Only then test bounded ambiguity classes:

  • both Transfer-Encoding and Content-Length;
  • repeated or comma-joined framing fields;
  • transfer-coding token case and exactness;
  • malformed chunk sizes and CRLF placement;
  • response ordering across two isolated test identities.

Stop when a differential is proven. Turning a marker into credential capture or cross-user impact adds risk without improving the root-cause finding. For tooling design, a Burp extension built for API security should preserve raw bytes and connection sequencing rather than silently canonicalize the probe before transmission.

Detection needs both sides of the boundary

Edge-only signatures are useful but incomplete. Alert on TE plus CL, duplicate Content-Length, unsupported transfer codings, invalid chunk-size bytes, and malformed CRLF. Retain the raw framing fields before normalization when privacy and storage controls permit.

The higher-confidence signal is disagreement telemetry. Correlate:

  • edge rejection or normalization with an origin request on the same upstream connection;
  • one client request ID with multiple origin request IDs;
  • origin routes that have no matching ingress transaction;
  • response-count or response-order anomalies on pooled connections;
  • abrupt backend resets following framing errors;
  • cache entries populated by a response whose origin request identity does not match the cache key transaction.

Propagate an immutable edge-generated request ID and overwrite any client-supplied copy. Log the upstream connection ID separately. Without connection identity, a suspicious origin request can look like an ordinary transaction from another user.

A practical defensive WAF validation should therefore include an origin observer. A rule that returns 400 is not a verified control if ambiguous bytes were already forwarded.

Fix the origin, then reduce parser diversity

Upgrade org.http4s:http4s-ember-core to at least 0.23.35 or 1.0.0-M47 for these HTTP/1.1 fixes. The release included a broader security-hardening set, so dependency resolution should verify the actual Ember artifact selected at runtime, not only the application’s top-level http4s declaration.[4]

At the intermediary, reject TE plus CL and duplicate length fields before routing. Reject unknown transfer codings. Prefer full request buffering and canonical re-encoding where latency and streaming requirements allow it. If an emergency control is needed, disabling backend connection reuse reduces cross-request impact, but it is a containment measure with capacity cost, not a parser fix.[1][2]

After patching, rerun the exact byte corpus through every production ingress path: CDN, WAF, load balancer, service-mesh sidecar, API gateway, and direct origin listener if one exists. Compare stable rejection behavior and confirm that no marker request appears at the origin. Parser security is a property of the deployed chain.

Strategic takeaways

The small details were a capital C, a substring, a plus sign, and one CRLF. None looks like an authorization primitive. On a shared byte stream, each can decide which user’s request a server believes it is reading.

Treat HTTP framing as a trust boundary with two owners. AppSec needs the raw parser corpus; platform engineering needs forwarding and pooling evidence; detection engineering needs ingress-to-origin correlation. If those teams validate their component in isolation, both parsers can pass their tests while the connection between them remains exploitable.

The durable control is not a longer denylist. It is one unambiguous grammar, strict failure, and evidence that every hop consumed the same message.

Sources

💜 Enjoyed this content? Support the blog with USDT (TRC20):

TX7obcjHQbDUXb4mGqoASEu1QFTKT2CFGG

View support page

Paulo Rigonato

Security Engineer | Red Team | Pentest

Offensive security specialist with experience in assessments, pentesting, and Red Team operations. He works in enterprise cybersecurity and continues to share knowledge through this blog.

Certifications: OSCP | eWPTXv2 | ITILv4

💻 GitHub 🔗 LinkedIn

Similar Posts