Bug 2536970 (CVE-2026-93579) - CVE-2026-93579 io.netty/netty-codec-http2: Netty: HTTP/2 header field values are not validated by default (CR/LF/NUL passthrough)
Summary: CVE-2026-93579 io.netty/netty-codec-http2: Netty: HTTP/2 header field values ...
Keywords:
Status: NEW
Alias: CVE-2026-93579
Product: Security Response
Classification: Other
Component: vulnerability
Version: unspecified
Hardware: All
OS: Linux
medium
medium
Target Milestone: ---
Assignee: Product Security
QA Contact:
URL:
Whiteboard:
Depends On:
Blocks:
TreeView+ depends on / blocked
 
Reported: 2026-09-18 10:36 UTC by OSIDB Bzimport
Modified: 2026-09-18 16:34 UTC (History)
66 users (show)

Fixed In Version:
Clone Of:
Environment:
Last Closed:
Embargoed:


Attachments (Terms of Use)

Description OSIDB Bzimport 2026-09-18 10:36:25 UTC
HTTP/2 header field values are not validated by default (CR/LF/NUL passthrough)

A public GitHub Security Advisory (GHSA-8whp-c7w8-2m72) describes the following issue:

## Summary

Netty's HTTP/2 stack does not validate header field **values** by default. Genuine
prohibited octets — NUL (0x00), LF (0x0A), CR (0x0D) — can be set in an HTTP/2 header
value and are carried verbatim to the wire (outbound) and into decoded headers (inbound).
This violates RFC 9113 §8.2.1 and becomes an exploitable request-smuggling / header-
injection / response-splitting vector at an HTTP/2 ↔ HTTP/1.1 translation boundary.

This report deliberately **reframes** an earlier report that blamed
`io.netty.util.AsciiString.c2b(char)`. `c2b` is not the problem — see "Non-issue:
c2b" below. The real gap is the absence of field-value validation in the HTTP/2 header
layer.

## Impact

- Severity: depends on deployment. Low on a pure end-to-end HTTP/2 hop (HPACK is
  length-prefixed, so embedded CR/LF do not split fields on the H2 wire).
- Elevated to request smuggling / response splitting whenever a value crosses into
  HTTP/1.1 (proxy / gateway / adapter) where CR/LF/COLON are delimiters.
- RFC 9113 §8.2.1: *"Failure to validate fields can be exploited for request smuggling
  attacks. ... A field value MUST NOT contain the zero value (ASCII NUL, 0x00), line
  feed (ASCII LF, 0x0a), or carriage return (ASCII CR, 0x0d) at any position."* Such
  messages MUST be treated as malformed, and non-tunnelling intermediaries MUST NOT
  forward fields containing these octets. RFC 7540 §10.3 has equivalent language.

## Root cause: value validation is opt-in and off by default

Header **names** are validated by default and are not affected:
`DefaultHttp2Headers` `HTTP2_NAME_VALIDATOR` (active when `validate=true`, the default)
runs `HttpHeaderValidationUtil.validateToken`, which rejects control chars, SP,
uppercase, non-ASCII, and (for `CharSequence`) any char `> 0xFF`.

Header **values** are only checked by an opt-in validator that is disabled by default:

- `DefaultHttp2Headers()` and `DefaultHttp2Headers(boolean)` install
  `ValueValidator.NO_VALIDATION`.
- Only `DefaultHttp2Headers(boolean validate, boolean validateValues, int)` installs the
  real `VALUE_VALIDATOR` (which calls `HttpHeaderValidationUtil.validateValidHeaderValue`,
  correctly rejecting `< 0x20` except HTAB, and 0x7F — i.e. CR/LF/NUL).
- `DefaultHttp2HeadersDecoder` defaults `validateHeaderValues = false`.
- No public builder exposes value validation. `Http2FrameCodecBuilder` /
  `AbstractHttp2ConnectionHandlerBuilder.validateHeaders` (default `true`) is wired
  **only** to the decoder's *name* validator, never to value validation and never to the
  encoder.

The outbound encode path performs no value validation at any stage:

```
Http2FrameCodec.writeHeadersFrame
  -> DefaultHttp2ConnectionEncoder.writeHeaders0   (validateHeadersSentState: stream lifecycle only)
  -> DefaultHttp2FrameWriter.writeHeadersInternal  (stream id / padding / weight only)
  -> DefaultHttp2HeadersEncoder / HpackEncoder.encodeHeaders   (serializes as-is)
```

Because CR (0x0D), LF (0x0A), and NUL (0x00) are all ≤ 255, they survive the
char→byte conversion unchanged and are emitted verbatim.

## Where it becomes exploitable (H2 → H1.1 translation)

`HttpConversionUtil.translateHeaders()` copies values with raw `output.add(name, value)`
and performs no CR/LF scan of its own. Whether the prohibited octets are caught depends
entirely on whether the destination HTTP/1.1 `HttpHeaders` has value validation enabled:

- `Http2StreamFrameToHttpObjectCodec(boolean isServer)` defaults `validateHeaders=true`
  → the resulting HTTP/1.1 headers use `DEFAULT_VALUE_VALIDATOR` → protected.
- `InboundHttp2ToHttpAdapter` takes `validateHttpHeaders` from its builder. If set
  `false`, an HTTP/2 value containing CR+LF flows verbatim into the HTTP/1.1 message,
  enabling header injection / request smuggling / response splitting.

## Non-issue: `AsciiString.c2b()` is a clamp, not a security boundary

```java
private static final char MAX_CHAR_VALUE = 255;
public static byte c2b(char c) { return (byte) ((c > MAX_CHAR_VALUE) ? '?' : c); }
```

The earlier report proposed changing `c2b`. That is misdirected:

1. **C1 control passthrough (0x80–0x9F) is spec-compliant.** These octets are `obs-text`
   (%x80–FF), valid in field values. HPACK is length-prefixed, so they are never
   delimiters on the HTTP/2 wire. No action needed.
2. **The `> 255 → '?'` replacement is protective, not harmful.** It prevents the
   "ghost bits" truncation attack: were `c2b` to truncate raw (as the private,
   *unreachable* `c2b0` does), `\u010A` (266) would become `0x0A` (LF) and `\u010D` (269)
   would become `0x0D` (CR) on the wire — actual injection. The clamp closes that hole.
   The proposed "reject C1 in c2b" change adds no security and would break obs-text.
3. The silent `?` substitution of `> 255` chars is at most a correctness/interop concern
   (application String vs. wire bytes mismatch); it makes values safer, not more
   dangerous, and is not a vulnerability.

Fixing `c2b` neither addresses the real gap nor is necessary. The fix belongs in the
HTTP/2 header layer.

## Affected code

- `io.netty.handler.codec.http2.DefaultHttp2Headers` — value validator off in the
  common constructors.
- `io.netty.handler.codec.http2.DefaultHttp2HeadersDecoder` — `validateHeaderValues`
  defaults to `false`.
- `io.netty.handler.codec.http2.Http2FrameCodecBuilder` /
  `AbstractHttp2ConnectionHandlerBuilder` — no way to enable value validation; the
  `validateHeaders` flag reaches only the decoder's name check.
- Outbound: `DefaultHttp2ConnectionEncoder`, `DefaultHttp2FrameWriter`,
  `HpackEncoder` — no value validation on encode.
- Translation: `HttpConversionUtil.translateHeaders`,
  `InboundHttp2ToHttpAdapter` (when `validateHttpHeaders=false`).

## Suggested fix

1. Validate HTTP/2 field values against RFC 9113 §8.2.1 — reject NUL (0x00), LF (0x0A),
   CR (0x0D) at any position (the logic alread

[truncated]

Affected:
- maven:io.netty:netty-codec-http2 affected >= 4.2.0.Final, <=4.2.17.Final; fixed unknown
- maven:io.netty:netty-codec-http2 affected <=4.1.137.Final; fixed unknown

Fixed versions: see advisory

Advisory: https://github.com/netty/netty/security/advisories/GHSA-8whp-c7w8-2m72


Note You need to log in before you can comment on or make changes to this bug.