Skip to content

fix: prevent CRLF injection through chunk trailers and extensions - #1256

Open
pjfanning wants to merge 1 commit into
apache:mainfrom
pjfanning:guard-chunk-trailer-crlf
Open

fix: prevent CRLF injection through chunk trailers and extensions#1256
pjfanning wants to merge 1 commit into
apache:mainfrom
pjfanning:guard-chunk-trailer-crlf

Conversation

@pjfanning

Copy link
Copy Markdown
Member

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:

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:

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:

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

Motivation:
`RenderSupport.renderChunk` emitted two attacker-influenced parts of a
chunked response without the CR/LF guard that the main header block
relies on:

- Trailer headers were rendered with `r ~~ trailer`, which resolves to
  the generic sequence renderer and calls `header.render` directly,
  bypassing the `~~(HttpHeader)` overload whose `check` scans the
  rendered bytes for CR/LF and drops the header. `HttpEntity.LastChunk`
  and `RawHeader` do no CR/LF validation, so a trailer built from user
  data (e.g. `RawHeader("X-Trace", "ok\r\nSet-Cookie: ..."))`) could
  split the response. The identical header placed in the main header
  block is caught; only the trailer path let it through.
- The chunk extension was rendered raw into the chunk-size line
  (`r ~~ ';' ~~ extension`) with no CR/LF check, so a CR/LF in an
  app-set extension corrupted the chunk framing.

Modification:
Render each trailer header through the guarded `~~(HttpHeader)`
overload (`trailer.foreach(r ~~ _)`), matching the main header block,
and remove the now-unused `trailerRenderer` implicit so the unguarded
path cannot be reintroduced by accident. Skip a chunk extension that
contains CR/LF; the extension is optional metadata, so omitting an
illegal one is safe. Byte output is unchanged for valid trailers and
extensions.

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, 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 a CRLF-bearing trailer header and a CRLF-bearing chunk extension are dropped. Verified both fail with the fix stashed (the injected bytes reach the output).
- sbt http-core/mimaReportBinaryIssues - pass (internal impl.engine.rendering change, no public API).

References:
None - closes the CRLF-injection paths in chunked response rendering
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant