pjfanning opened a new pull request, #1256:
URL: https://github.com/apache/pekko-http/pull/1256

   ### Motivation
   
   `RenderSupport.renderChunk` emitted two attacker-influenceable parts of a 
chunked response **without** the CR/LF guard that the main header block relies 
on. The guard lives in the `~~(header: HttpHeader)` overload 
(`Rendering.scala`), which marks the position, renders the header, and then 
scans the rendered bytes for CR/LF — dropping the header if any are found. Only 
the main header loop went through it.
   
   **Trailer headers** were rendered with `r ~~ trailer`, which resolves to the 
generic sequence renderer (`Renderer[immutable.Iterable[HttpHeader]]`) and 
calls `header.render(r)` directly, bypassing the guard:
   
   ```scala
   case HttpEntity.LastChunk(_, trailer) => r ~~ trailer ~~ CrLf
   ```
   
   `HttpEntity.LastChunk` and `RawHeader` perform no CR/LF validation, so a 
trailer built from user data splits the response:
   
   ```scala
   HttpEntity.Chunked(ct, Source(List(
     HttpEntity.Chunk("body"),
     HttpEntity.LastChunk(trailer = List(RawHeader("X-Trace", 
"ok\r\nSet-Cookie: session=attacker"))))))
   ```
   
   The identical `RawHeader` placed in the normal header list **is** caught by 
the guard; only because it rides in the trailer did it reach the wire.
   
   **The chunk extension** was rendered raw into the chunk-size line with no 
CR/LF check:
   
   ```scala
   if (extension.nonEmpty) r ~~ ';' ~~ extension
   ```
   
   so a CR/LF in an app-set `HttpEntity.Chunk(data, extension)` corrupted the 
chunk framing.
   
   ### Modification
   
   - Render each trailer header through the guarded `~~(HttpHeader)` overload 
(`trailer.foreach(r ~~ _)`), matching how the main header block renders 
(`HttpResponseRendererFactory.render(h) = r ~~ h`).
   - Remove the now-unused `trailerRenderer` implicit, so the unguarded 
`Renderer[Iterable[HttpHeader]]` cannot be picked up again by accident.
   - Skip a chunk extension containing CR/LF; the extension is optional 
metadata, so omitting an illegal one is safe.
   
   Byte output is unchanged for valid trailers and extensions (verified by the 
existing rendering tests, which are exact-string comparisons).
   
   ### Result
   
   CR/LF in a chunk trailer header value or a chunk extension can no longer 
reach the wire — the offending header/extension is dropped, exactly as in the 
main header block, instead of splitting the response.
   
   ### Tests
   
   - `sbt "http-core/testOnly 
org.apache.pekko.http.impl.engine.rendering.ResponseRendererSpec 
org.apache.pekko.http.impl.engine.rendering.RequestRendererSpec"` — pass (62 
tests). Two new tests assert that a CRLF-bearing trailer header and a 
CRLF-bearing chunk extension are dropped from the rendered output. Verified 
both fail with the fix stashed (the injected `Set-Cookie` bytes reach the 
output).
   - `sbt http-core/mimaReportBinaryIssues` — pass (internal 
`impl.engine.rendering` change, no public API).
   - Native `scalafmt` on the changed files — clean.
   
   ### References
   
   None - closes the CRLF-injection paths in chunked response rendering
   
   🤖 Generated with [Claude Code](https://claude.com/claude-code)


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to