A flaw was found in Netty. A remote unauthenticated attacker can exploit a vulnerability in Netty's HTTP/1 to HTTP/2 conversion process. When an HTTP/1 request includes both an absolute-form request-target and a conflicting Host header, Netty incorrectly prioritizes the Host header for the HTTP/2 :authority field, discarding the original request-target authority. This inconsistency can allow an attacker to bypass security controls in Netty-based proxies or gateways, potentially leading to unauthorized access, cache poisoning, or misrouting of requests.
Netty is a network application framework for development of protocol servers and clients. In netty-codec-http2 prior to versions 4.1.135.Final and 4.2.15.Final, the DelegatingDecompressorFrameListener class orchestrates HTTP/2 decompression by embedding a per-stream EmbeddedChannel that runs the appropriate decompression codec (gzip, deflate, zstd) and forwards decompressed chunks to a wrapped listener. Each decompressed chunk is a pooled ByteBuf handed to an anonymous ChannelInboundHandlerAdapter tail handler, which becomes the sole owner responsible for releasing it. A remote peer could send frames that would result in the flow-controller throwing and so trigger a resource leak which at the end might take down the whole JVM due OOME. Versions 4.1.135.Final and 4.2.15.Final patch the issue.
HTTP/1 absolute-form Host mismatch is translated to HTTP/2 :authority, overriding the request-target authority
A public GitHub Security Advisory (GHSA-cg2g-fxr4-mg8m) describes the following issue:
Summary
HttpConversionUtil.toHttp2Headers(...) translates an HTTP/1 request with an absolute-form request-target and a conflicting Host header into HTTP/2 using the Host header as :authority.
For example, this raw HTTP/1 request:
http GET http://request-target.example/admin HTTP/1.1 Host: host-header.example
is translated by Netty into HTTP/2 control data with:
text :scheme = http :path = /admin :authority = host-header.example
The request-target authority request-target.example is discarded because Netty only takes the absolute-form request-line authority when the Host header is empty.
This creates a host/authority confusion primitive in Netty-based HTTP/1 to HTTP/2 proxy or gateway pipelines. A security decision made against the absolute-form request-target authority can be bypassed when Netty forwards the request over HTTP/2 using attacker-controlled Host as :authority.
Technical Details
In codec-http2/src/main/java/io/netty/handler/codec/http2/HttpConversionUtil.java, toHttp2Headers(...) initializes host from the HTTP/1 Host header:
java String host = inHeaders.getAsString(HttpHeaderNames.HOST);
For non-origin-form requests, it parses the request target and sets the HTTP/2 path:
java String requestTarget = request.uri(); out.path(toHttp2Path(requestTarget));
When the request-target has a scheme and authority, Netty parses the scheme/authority portion, but only uses that authority if Host was empty:
java URI requestTargetUri = URI.create(http2PathlessRequestTarget(requestTarget)); // Take from the request-line if HOST header was empty host = isNullOrEmpty(host) ? requestTargetUri.getAuthority() : host; setHttp2Scheme(inHeaders, requestTargetUri, out);
Finally, Netty emits :authority from host:
java setHttp2Authority(host, out);
The result is that a conflicting Host header overrides the authoritative absolute-form request-target authority during HTTP/1 to HTTP/2 conversion.
Impact
In a Netty-based proxy or gateway that accepts HTTP/1, performs access control, routing, tenant selection, egress allowlisting, or cache-keying based on the absolute-form request-target, and then forwards the request over HTTP/2 using HttpConversionUtil.toHttp2Headers(...), an attacker can send:
http GET http://allowed.example/admin HTTP/1.1 Host: blocked-or-attacker.example
The gateway can approve the request based on allowed.example, while Netty emits an HTTP/2 request with :authority=blocked-or-attacker.example.
This can bypass host or tenant security boundaries, poison cross-host cache entries, or route requests to an unintended upstream. The primitive is not a generic application Host-header trust issue; it is a protocol-conversion inconsistency inside Netty's HTTP/1 to HTTP/2 translation helper.
Suggested Fix
Recommended fix direction:
- If the HTTP/1 request-target is absolute-form and contains an authority component, construct HTTP/2 :authority from that request-target authority. - Alternatively, reject conversion when absolute-form request-target authority and Host differ after scheme-based normalization. - Preserve current origin-form behavior where Host is the authority source. - Add regression tests for mismatched absolute-form authority/Host, matching authority/Host, origin-form Host, userinfo stripping, IPv6 literals, and explicit ports.
Affected Package/Versions
Primary package: io.netty:netty-codec-http2
Related package: io.netty:netty-codec-http
Confirmed affected:
- current 4.2 branch at 7bae566a93e69409697fe57fa807910ba5c9720e - 4.2.15.Final at a41f7b289ce1d697c50846f3ade3983e22b2ed40 - 4.1.135.Final at f05f765d81460799c53123a207f665bf3b465171
Suggested affected ranges:
- >= 4.1.0.Final, <= 4.1.135.Final - >= 4.2.0.Final, <= 4.2.15.Final
References
- Netty security policy: https://github.com/netty/netty/security/policy - RFC 9112 request-target and absolute-form rules: https://datatracker.ietf.org/doc/html/rfc9112#section-3.2 - RFC 9113 request pseudo-header fields: https://datatracker.ietf.org/doc/html/rfc9113#section-8.3.1 - RFC 9110 http URI authority syntax: https://datatracker.ietf.org/doc/html/rfc9110#section-4.2.1 - HTTPWG mismatching absolute URI and Host discussion: https://github.com/httpwg/http-core/issues/191 - HTTPWG absolute-form precedence clarification: https://github.com/httpwg/http-core/issues/1105 - Netty HTTP/2 content-length request smuggling advisory: https://github.com/advisories/GHSA-f256-j965-7f32 - Netty HTTP/2 request-smuggling validation advisory: https://github.com/advisories/GHSA-wm47-8v5p-wjpj
CWE and CVSS
Primary CWE: CWE-444 (Inconsistent Interpretation of HTTP Requests).
Secondary CWE: CWE-20 (Improper Input Validation / protocol normalization).
Suggested CVSS v3.1:
text CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:N
Suggested severity: High, score 8.1.
Rationale: a remote unauthenticated HTTP/1 client can trigger the conversion differential in a gateway/proxy deployment. The strongest impact is integrity: host, tenant, egress, or route policy can be checked against one authority and forwarded over HTTP/2 to another. Availability impact is not claimed.
Duplicate Boundary
Adjacent public Netty advisories cover different surfaces:
- GHSA-f256-j965-7f32 / CVE-2021-21409: HTTP/2 request smuggling due content-length validation. - GHSA-wm47-8v5p-wjpj / CVE-2021-21295: HTTP/2 request smuggling due missing validation. - GHSA-c7h2-758g-pf5v: private triage item for HTTP/2 :path HTAB surviving HTTP/1 downgrade.
This candidate is HTTP/1 absolute-form to HTTP/2 :authority translation. It does not rely on content-length ambiguity, HTTP/2-to-HTTP/1 downgrade, or request-line whitespace.
Local PoV
PoV file in the local bundle:
po
[truncated]
Affected: - maven:io.netty:netty-codec-http2 affected >= 4.1.0.Final, <= 4.1.137.Final; fixed unknown - maven:io.netty:netty-codec-http2 affected >= 4.2.0.Final, <= 4.2.17.Final; fixed unknown
Fixed versions: see advisory
Advisory: https://github.com/netty/netty/security/advisories/GHSA-cg2g-fxr4-mg8m
HTTP/1 authority-form CONNECT is translated to malformed HTTP/2 CONNECT with Host-controlled :authority
A public GitHub Security Advisory (GHSA-45h4-vhwh-fmhg) describes the following issue:
Summary
Netty's HTTP/1-to-HTTP/2 conversion does not special-case true HTTP/1 CONNECT authority-form request-targets. Instead, HttpConversionUtil.toHttp2Headers() applies the generic request conversion path to a request-target like trusted.example:443.
For example:
text CONNECT trusted.example:443 HTTP/1.1 Host: attacker.example:443
is converted to HTTP/2 headers with these security-relevant properties:
text :method: CONNECT :authority: attacker.example:443 :scheme: trusted.example :path: <present>
The exact :path value has varied across releases (/ in checked older 4.2.x releases and trusted.example:443 in current/4.2.15), but the invariant bug is stable: the CONNECT tunnel authority is taken from the HTTP/1 Host header instead of the CONNECT authority-form request-target, and Netty emits HTTP/2 CONNECT-forbidden :scheme and :path pseudo-headers.
HTTP/2 CONNECT must omit :scheme and :path, and :authority must contain the host and port from the CONNECT authority-form request-target. In a Netty-based HTTP/1-to-HTTP/2 proxy or gateway, policy that authorizes the HTTP/1 CONNECT request-target can disagree with the upstream HTTP/2 proxy, which receives the Host-controlled :authority.
Technical Details
The relevant code is codec-http2/src/main/java/io/netty/handler/codec/http2/HttpConversionUtil.java. toHttp2Headers(HttpMessage, boolean) handles HttpRequest conversion with a generic URI/request-target parser:
text String host = inHeaders.getAsString(HttpHeaderNames.HOST); ... String requestTarget = request.uri(); out.path(toHttp2Path(requestTarget)); ... setHttp2Scheme(...); setHttp2Authority(host, out); out.method(request.method().asciiName());
For CONNECT, the HTTP/1 request-target is authority-form (host:port), not an origin-form path and not an absolute URI. On the current branch, trusted.example:443 is treated as a scheme-like string for :scheme, the request-target is emitted as :path, and the HTTP/1 Host header is used for :authority. On checked older 4.2.x releases, the :path value is /, but the authority confusion and forbidden pseudo-header emission still reproduce.
Impact
In a Netty HTTP/1-to-HTTP/2 proxy/gateway path, a remote HTTP/1 client can ask to CONNECT to one authority while supplying a different Host header. Netty then builds an HTTP/2 CONNECT request whose tunnel :authority is Host-controlled and whose pseudo-header set is malformed.
This can bypass tunnel allow-lists, egress policy, backend selection, audit logic, or other security controls that validate the HTTP/1 CONNECT request-target before forwarding over HTTP/2.
This report does not claim code execution or memory corruption. The impact is integrity loss through CONNECT tunnel-target confusion at an HTTP/1-to-HTTP/2 conversion boundary.
Suggested Fix
When converting true HTTP/1 authority-form CONNECT to HTTP/2:
- set :method to CONNECT; - set :authority from the HTTP/1 CONNECT request-target authority-form; - omit :scheme and :path; - reject or ignore conflicting Host instead of allowing it to replace the CONNECT target; - add regression tests for direct toHttp2Headers(...) and outbound HttpToHttp2ConnectionHandler conversion.
The attached patched-control diff demonstrates the minimal behavior change:
evidence/minimal-connect-patched-control.diff
The patched-control run passed:
fish ./mvnw -q -pl codec-http2 \ -Dtest=Http2ConnectAuthorityFormFixedControlTest,HttpToHttp2ConnectionHandlerTest#testAuthorityFormRequestTargetHandled \ -Dsurefire.failIfNoSpecifiedTests=false \ -DskipNativeTests -DskipAutobahnTests -Dmaven.antrun.skip=true test
Affected Package/Versions
io.netty:netty-codec-http2
Confirmed affected:
- current 4.2 branch at 7bae566a93e69409697fe57fa807910ba5c9720e - 4.2.15.Final at a41f7b289ce1d697c50846f3ade3983e22b2ed40 - 4.2.2.Final at 660edeaefad4a4cedbd61584e8f668ad2d89f0b8 - 4.2.0.Final at 09e64d259c99be8b5b2a471a78f11e65eb82598a - 4.1.135.Final at f05f765d81460799c53123a207f665bf3b465171
Suggested affected ranges:
- >= 4.1.0.Final, <= 4.1.135.Final - >= 4.2.0.Final, <= 4.2.15.Final
References
- Netty security policy: https://github.com/netty/netty/security/policy - RFC 9113 HTTP/2 CONNECT method: https://datatracker.ietf.org/doc/html/rfc9113#section-8.5 - RFC 9112 authority-form: https://datatracker.ietf.org/doc/html/rfc9112#section-3.2.3 - RFC 9110 CONNECT method: https://datatracker.ietf.org/doc/html/rfc9110#section-9.3.6
CWE and CVSS
Suggested CWEs:
- CWE-20: Improper Input Validation - CWE-436: Interpretation Conflict - CWE-444: Inconsistent Interpretation of HTTP Requests
Suggested CVSS v3.1:
text CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N
Suggested severity: High, score 7.5.
Rationale: a network peer can trigger this in a proxy/gateway path without authentication. Integrity impact is high because CONNECT can establish a tunnel to an unintended upstream authority when forwarding policy validated a different request-target.
Duplicate Boundary
Nearby reports are distinct:
- GHSA-cg2g-fxr4-mg8m: HTTP/1 absolute-form Host mismatch to HTTP/2 :authority. - GHSA-jgph-cgq3-c627: the sibling HTTP/1 CONNECT authority-form conversion bug in netty-codec-http3. - GHSA-w6j8-x45j-w75f: HTTP/2/HTTP/3 Extended CONNECT inbound downgrade. - GHSA-gcjj-c5ff-2m72, GHSA-xf5f-3m33-p8m3, and GHSA-w424-c27v-r9mr: HTTP/2-to-HTTP/1 inbound downgrade families.
This report covers outbound HTTP/1 CONNECT authority-form conversion to HTTP/2.
Live duplicate evidence is in evidence/advisory-duplicate-check.tsv.
Local PoV
PoV file:
pov/Http2ConnectAuthorityFormHostConfusionPovTest.java
Run from a Netty checkout after copying the PoV file into codec-http2/src/test/java/io/
[truncated]
Affected: - maven:io.netty:netty-codec-http2 affected >= 4.1.0.Final, <= 4.1.137.Final; fixed unknown - maven:io.netty:netty-codec-http2 affected >= 4.2.0.Final, <= 4.2.17.Final; fixed unknown
Fixed versions: see advisory
Advisory: https://github.com/netty/netty/security/advisories/GHSA-45h4-vhwh-fmhg
A flaw was found in Netty's HTTP/2 HpackEncoder. A remote attacker can exploit this by sending HTTP/2 SETTINGS frames with a very large MAXHEADERTABLESIZE. This causes the HpackEncoder to store an excessive number of unique headers, leading to increased CPU usage and memory consumption, ultimately resulting in a Denial of Service (DoS).
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 HTTP2NAMEVALIDATOR (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.NOVALIDATION. - Only DefaultHttp2Headers(boolean validate, boolean validateValues, int) installs the real VALUEVALIDATOR (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 DEFAULTVALUEVALIDATOR → 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 MAXCHARVALUE = 255; public static byte c2b(char c) { return (byte) ((c > MAXCHARVALUE) ? '?' : 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