CWICP study materials
CWICP study resources

Ambiguous Framing on a Three-Tier Booking Stack

CWICP · Put your knowledge to work in context.

All case studies

Ambiguous Framing on a Three-Tier Booking Stack

You are engaged by a travel-booking company to test booking.example.com. The stack is CloudFront in front of an AWS ALB, then nginx 1.24, then Node.js 20. The rules of engagement prohibit denial-of-service testing and any action that could affect live customers. While probing POST /api/cart/coupon you notice that requests carrying both Content-Length and Transfer-Encoding: chunked are rejected by CloudFront with a 400. A variant with a space before the colon - Transfer-Encoding : chunked - is forwarded instead. Two seconds later, a second request you pipelined on the same connection, GET /nonexistent-7f3a2b, returns a 404 with a Node.js stack trace, even though that path was never sent to the origin as its own request. Separately, a smuggled prefix carrying a 200 KB body makes the following response stall for about 12 seconds. The client's WAF vendor states that it has full request-smuggling coverage, and the client is relying on that statement.

1. How do you confirm this is a genuine desync rather than a caching or timing artifact, and what do you deliberately not repeat?

Read the worked answer

What the evidence already shows. A 404 for GET /nonexistent-7f3a2b that you never sent as its own request means the origin parsed a request boundary that the edge did not. That is the desync primitive. It is not proof of cross-user impact, and it is not proof that a victim can be reached.

A clean, self-contained confirmation.

  1. Send both requests over one TCP connection (in Burp: send group in sequence, single connection), with the smuggled prefix shaped as GET /nonexistent-<random> HTTP/1.1, a correct Host, and a Connection: keep-alive line, terminated so the trailing bytes form a complete request.
  2. Use a fresh random path on every attempt, and add Cache-Control: no-store to the second request. If a path that has never been requested normally returns a 404, no cache can explain it.
  3. Request the same path normally first and note the generic nginx 404, then compare with the Node.js stack trace from the smuggled request - that shows the bytes reached the application tier.
  4. Repeat three times and vary chunk sizes. Deterministic reproduction excludes jitter.
  5. Capture a pcap alongside Burp so the report contains raw bytes rather than a tool rendering.

What I would not repeat. The 200 KB variant. A 12-second stall means requests are queuing, and queuing is precisely the state in which a poisoned upstream connection can be handed to another user. Re-running it is a load test on a live system: outside the RoE and unnecessary, because the primitive is already proven.

Cheap additional tests. Which exact obfuscation is accepted (Transfer-Encoding : chunked, tab-separated, duplicated header), and whether an HTTP/2 request to the edge triggers the same mismatch. That tells the client which tier to patch first.

2. The WAF vendor says request smuggling is covered. How do you rate and write this up, and what do you recommend?

Read the worked answer

Rating. I would report High, with an explicit note that Critical is plausible but unproven. Reasoning: I have demonstrated a desync primitive between the edge and the application tier, which is the precondition for request hijacking, cache poisoning and front-end auth bypass. I have not demonstrated that a smuggled request can be delivered to another user's connection, because doing so would require exactly the queuing behaviour I stopped short of testing. Severity claims must track what was proven, not what is imaginable.

The WAF argument, made precisely. The vendor's coverage is for well-formed TE.CL and CL.TE pairs. The bypass is header obfuscation - a space before the colon - which changes how the header is tokenised rather than what it says. CloudFront and the ALB did not normalise it and did not reject it, so a fully-covered WAF signature never saw an ambiguous request. The correct sentence in the report is: the WAF is not a compensating control here, because the ambiguity is created below the layer at which it inspects.

Evidence to attach. The obfuscated request bytes, the 404 on the random path, the pcap, the normal 404 for comparison, and a note that the high-volume variant was intentionally not repeated.

Recommendations, in order of durability.

  • Reject any request that carries both Content-Length and Transfer-Encoding at the edge, before normalisation, and normalise header whitespace.
  • Terminate HTTP/2 at the edge and speak HTTP/2 or a normalised HTTP/1.1 to the origin, so header ambiguity cannot cross the boundary.
  • Disable upstream keep-alive reuse between nginx and Node, or use a version of each that agrees on framing.
  • Patch and re-test each hop independently: fix nginx and the front end may still disagree in the other direction.

Retest. Re-run the obfuscated variants and confirm a 400 at the edge, and confirm the smuggled path is never answered.