Bug 2536970 (CVE-2026-93579)

Summary: CVE-2026-93579 io.netty/netty-codec-http2: Netty: HTTP/2 header field values are not validated by default (CR/LF/NUL passthrough)
Product: [Other] Security Response Reporter: OSIDB Bzimport <bzimport>
Component: vulnerabilityAssignee: Product Security <prodsec-ir-bot>
Status: NEW --- QA Contact:
Severity: medium Docs Contact:
Priority: medium    
Version: unspecifiedCC: ant, anujha, aschwart, asoldano, aszczucz, ataylor, bbaranow, bmaxwell, boliveir, bstansbe, ccranfor, cescoffi, csuconic, dbruscin, dlofthou, drichtar, ehugonne, ewittman, fmariani, fmongiar, gmalinko, gsmet, gtully, istudens, ivassile, iweiss, janstey, jmartisk, jnethert, jpechane, jsherman, jwon, kvanderr, manderse, mcarlett, mosmerov, mposolda, msvehla, nipatil, nwallace, olubyans, ozzy, pantinor, pberan, pdelbell, pesilva, pjindal, pmackay, rgemmell, rgodfrey, rguimara, rkubis, rmartinc, rstancel, rstepani, sbiarozk, ssilvert, sthorger, tbish, tcunning, thjenkin, tlavocat, varjain, vdosoudi, vmuzikar, yfang
Target Milestone: ---Keywords: Security
Target Release: ---   
Hardware: All   
OS: Linux   
Whiteboard:
Fixed In Version: Doc Type: ---
Doc Text:
A flaw was found in Netty's HTTP/2 stack. This vulnerability allows a remote attacker to inject prohibited characters, such as NUL, Line Feed, and Carriage Return, into HTTP/2 header field values due to insufficient validation. When these values cross an HTTP/2 to HTTP/1.1 translation boundary, they can be exploited for request smuggling, header injection, or response splitting. This could lead to unauthorized access, data manipulation, or other security bypasses.
Story Points: ---
Clone Of: Environment:
Last Closed: Type: ---
Regression: --- Mount Type: ---
Documentation: --- CRM:
Verified Versions: Category: ---
oVirt Team: --- RHEL 7.3 requirements from Atomic Host:
Cloudforms Team: --- Target Upstream Version:
Embargoed:

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