{"id":"GHSA-hvcg-qmg6-jm4c","summary":"Netty: HttpObjectDecoder skips arbitrary initial control characters when only initial CRLF characters are permitted","details":"## Summary\n\nBefore reading the first request-line, `HttpObjectDecoder` skips every byte for which\n`Character.isISOControl(b)` is `true` (0x00–0x1F and 0x7F) as well as all whitespace.\nRFC 9112 §2.2 only asks servers to ignore **empty CRLF lines** preceding the request-line —\na carefully scoped robustness allowance intended to handle HTTP/1.0 POST workarounds.\nSilently absorbing NUL bytes, SOH, STX, and other non-CRLF control characters goes\nsignificantly beyond this, and can be exploited for request-boundary confusion in pipelined\nor multiplexed transports where a front-end component treats those bytes differently.\n\n## Affected Code\n\n| File | Lines | Role |\n|------|-------|------|\n| `codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java` | 1298–1313 | `ISO_CONTROL_OR_WHITESPACE` static initialiser — marks all ISO control chars |\n| `codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java` | 1307–1313 | `SKIP_CONTROL_CHARS_BYTES` `ByteProcessor` — skips the entire set |\n| `codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java` | 1275–1289 | `LineParser.skipControlChars` — advances `readerIndex` past all matching bytes |\n\n## Specification Analysis\n\n### RFC 9112 §2.2 — Message Parsing\n\n\u003e In the interest of robustness, a server that is expecting to receive and parse a\n\u003e request-line **SHOULD ignore at least one empty line (CRLF)** received prior to the\n\u003e request-line.\n\n\u003e An HTTP/1.1 user agent **MUST NOT** preface or follow a request with an extra CRLF.\n\n### Deviation\n\nThe RFC names a single permitted exception: an **empty line** (bare CRLF, i.e. the two-byte\nsequence `\\r\\n`).  The `ISO_CONTROL_OR_WHITESPACE` table is initialised as:\n\n```java\nfor (byte b = Byte.MIN_VALUE; b \u003c Byte.MAX_VALUE; b++) {\n    ISO_CONTROL_OR_WHITESPACE[128 + b] =\n        Character.isISOControl(b) || isWhitespace(b);\n}\n```\n\n`Character.isISOControl` returns `true` for `0x00`–`0x1F` and `0x7F`.  This includes NUL\n(`0x00`), SOH (`0x01`), STX (`0x02`), BEL (`0x07`), DEL (`0x7F`), and every other non-CRLF\ncontrol character.  The `SKIP_CONTROL_CHARS` state runs this scan unconditionally before the\nfirst `READ_INITIAL`, meaning any sequence of such bytes prepended to a request is silently\nconsumed.\n\nA load balancer or TLS terminator that does not perform the same scan sees a different\nmessage boundary than Netty does, which is the basis of a request-desync / smuggling attack.\n\n## Suggested Unit Test\n\nAdd to `HttpRequestDecoderTest.java`.\n\n```java\n@Test\npublic void testNonCrlfControlBytesPrecedingRequestLineAreRejected() {\n    // RFC 9112 §2.2: servers SHOULD ignore \"at least one empty line (CRLF)\" before the\n    // request-line.  Non-CRLF control bytes are not part of this robustness allowance\n    // and must not be silently swallowed.\n    EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder());\n\n    ByteBuf buf = Unpooled.buffer();\n    buf.writeByte(0x00);   // NUL  — not an empty CRLF line\n    buf.writeByte(0x01);   // SOH  — not an empty CRLF line\n    buf.writeCharSequence(\n            \"GET / HTTP/1.1\\r\\nHost: example.com\\r\\n\\r\\n\",\n            CharsetUtil.US_ASCII);\n\n    channel.writeInbound(buf);\n    HttpRequest req = channel.readInbound();\n\n    // Current behaviour: NUL and SOH are in ISO_CONTROL_OR_WHITESPACE, so they are\n    // silently skipped; the request decodes successfully and isFailure() == false.\n    //\n    // RFC-correct behaviour: only empty CRLF lines should be ignored; NUL/SOH must\n    // cause a parse error — isFailure() == true.\n    assertTrue(\n            req.decoderResult().isFailure(),\n            \"Non-CRLF control bytes before the request-line must not be silently skipped \" +\n            \"(RFC 9112 §2.2 allows only empty CRLF lines)\");\n\n    assertFalse(channel.finish());\n}\n```\n\n**Current behaviour (unfixed):** `skipControlChars` advances past `0x00` and `0x01` because\nboth are in `ISO_CONTROL_OR_WHITESPACE`; the request parses normally, `isFailure()` is\n`false` → test **fails**.\n\n**Expected behaviour after fix:** only CRLF empty lines are tolerated; non-CRLF control\nbytes produce an error, `isFailure()` is `true` → test **passes**.","aliases":["CVE-2026-50020"],"modified":"2026-07-17T21:17:27.497640580Z","published":"2026-06-15T20:46:36Z","database_specific":{"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-06-15T20:46:36Z","nvd_published_at":"2026-06-12T16:16:31Z","cwe_ids":["CWE-444"]},"references":[{"type":"WEB","url":"https://github.com/netty/netty/security/advisories/GHSA-hvcg-qmg6-jm4c"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-50020"},{"type":"PACKAGE","url":"https://github.com/netty/netty"},{"type":"WEB","url":"https://github.com/netty/netty/releases/tag/netty-4.1.135.Final"},{"type":"WEB","url":"https://github.com/netty/netty/releases/tag/netty-4.2.15.Final"}],"affected":[{"package":{"name":"io.netty:netty-codec-http","ecosystem":"Maven","purl":"pkg:maven/io.netty/netty-codec-http"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.2.0.Final"},{"fixed":"4.2.15.Final"}]}],"versions":["4.2.0.Final","4.2.1.Final","4.2.10.Final","4.2.11.Final","4.2.12.Final","4.2.13.Final","4.2.14.Final","4.2.2.Final","4.2.3.Final","4.2.4.Final","4.2.5.Final","4.2.6.Final","4.2.7.Final","4.2.8.Final","4.2.9.Final"],"database_specific":{"last_known_affected_version_range":"\u003c= 4.2.14.Final","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-hvcg-qmg6-jm4c/GHSA-hvcg-qmg6-jm4c.json"}},{"package":{"name":"io.netty:netty-codec-http","ecosystem":"Maven","purl":"pkg:maven/io.netty/netty-codec-http"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"4.1.135.Final"}]}],"versions":["4.0.0.Alpha1","4.0.0.Alpha2","4.0.0.Alpha3","4.0.0.Alpha4","4.0.0.Alpha5","4.0.0.Alpha6","4.0.0.Alpha7","4.0.0.Alpha8","4.0.0.Beta1","4.0.0.Beta2","4.0.0.Beta3","4.0.0.CR1","4.0.0.CR2","4.0.0.CR3","4.0.0.CR4","4.0.0.CR5","4.0.0.CR6","4.0.0.CR7","4.0.0.CR8","4.0.0.CR9","4.0.0.Final","4.0.1.Final","4.0.10.Final","4.0.11.Final","4.0.12.Final","4.0.13.Final","4.0.14.Beta1","4.0.14.Final","4.0.15.Final","4.0.16.Final","4.0.17.Final","4.0.18.Final","4.0.19.Final","4.0.2.Final","4.0.20.Final","4.0.21.Final","4.0.22.Final","4.0.23.Final","4.0.24.Final","4.0.25.Final","4.0.26.Final","4.0.27.Final","4.0.28.Final","4.0.29.Final","4.0.3.Final","4.0.30.Final","4.0.31.Final","4.0.32.Final","4.0.33.Final","4.0.34.Final","4.0.35.Final","4.0.36.Final","4.0.37.Final","4.0.38.Final","4.0.39.Final","4.0.4.Final","4.0.40.Final","4.0.41.Final","4.0.42.Final","4.0.43.Final","4.0.44.Final","4.0.45.Final","4.0.46.Final","4.0.47.Final","4.0.48.Final","4.0.49.Final","4.0.5.Final","4.0.50.Final","4.0.51.Final","4.0.52.Final","4.0.53.Final","4.0.54.Final","4.0.55.Final","4.0.56.Final","4.0.6.Final","4.0.7.Final","4.0.8.Final","4.0.9.Final","4.1.0.Beta1","4.1.0.Beta2","4.1.0.Beta3","4.1.0.Beta4","4.1.0.Beta5","4.1.0.Beta6","4.1.0.Beta7","4.1.0.Beta8","4.1.0.CR1","4.1.0.CR2","4.1.0.CR3","4.1.0.CR4","4.1.0.CR5","4.1.0.CR6","4.1.0.CR7","4.1.0.Final","4.1.1.Final","4.1.10.Final","4.1.100.Final","4.1.101.Final","4.1.102.Final","4.1.103.Final","4.1.104.Final","4.1.105.Final","4.1.106.Final","4.1.107.Final","4.1.108.Final","4.1.109.Final","4.1.11.Final","4.1.110.Final","4.1.111.Final","4.1.112.Final","4.1.113.Final","4.1.114.Final","4.1.115.Final","4.1.116.Final","4.1.117.Final","4.1.118.Final","4.1.119.Final","4.1.12.Final","4.1.120.Final","4.1.121.Final","4.1.122.Final","4.1.123.Final","4.1.124.Final","4.1.125.Final","4.1.126.Final","4.1.127.Final","4.1.128.Final","4.1.129.Final","4.1.13.Final","4.1.130.Final","4.1.131.Final","4.1.132.Final","4.1.133.Final","4.1.134.Final","4.1.14.Final","4.1.15.Final","4.1.16.Final","4.1.17.Final","4.1.18.Final","4.1.19.Final","4.1.2.Final","4.1.20.Final","4.1.21.Final","4.1.22.Final","4.1.23.Final","4.1.24.Final","4.1.25.Final","4.1.26.Final","4.1.27.Final","4.1.28.Final","4.1.29.Final","4.1.3.Final","4.1.30.Final","4.1.31.Final","4.1.32.Final","4.1.33.Final","4.1.34.Final","4.1.35.Final","4.1.36.Final","4.1.37.Final","4.1.38.Final","4.1.39.Final","4.1.4.Final","4.1.40.Final","4.1.41.Final","4.1.42.Final","4.1.43.Final","4.1.44.Final","4.1.45.Final","4.1.46.Final","4.1.47.Final","4.1.48.Final","4.1.49.Final","4.1.5.Final","4.1.50.Final","4.1.51.Final","4.1.52.Final","4.1.53.Final","4.1.54.Final","4.1.55.Final","4.1.56.Final","4.1.57.Final","4.1.58.Final","4.1.59.Final","4.1.6.Final","4.1.60.Final","4.1.61.Final","4.1.62.Final","4.1.63.Final","4.1.64.Final","4.1.65.Final","4.1.66.Final","4.1.67.Final","4.1.68.Final","4.1.69.Final","4.1.7.Final","4.1.70.Final","4.1.71.Final","4.1.72.Final","4.1.73.Final","4.1.74.Final","4.1.75.Final","4.1.76.Final","4.1.77.Final","4.1.78.Final","4.1.79.Final","4.1.8.Final","4.1.80.Final","4.1.81.Final","4.1.82.Final","4.1.83.Final","4.1.84.Final","4.1.85.Final","4.1.86.Final","4.1.87.Final","4.1.88.Final","4.1.89.Final","4.1.9.Final","4.1.90.Final","4.1.91.Final","4.1.92.Final","4.1.93.Final","4.1.94.Final","4.1.95.Final","4.1.96.Final","4.1.97.Final","4.1.98.Final","4.1.99.Final"],"database_specific":{"last_known_affected_version_range":"\u003c= 4.1.134.Final","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-hvcg-qmg6-jm4c/GHSA-hvcg-qmg6-jm4c.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N"}]}