GHSA-HVCG-QMG6-JM4C

Vulnerability from github – Published: 2026-06-15 20:46 – Updated: 2026-06-15 20:46
VLAI
Summary
Netty: HttpObjectDecoder skips arbitrary initial control characters when only initial CRLF characters are permitted
Details

Summary

Before reading the first request-line, HttpObjectDecoder skips every byte for which Character.isISOControl(b) is true (0x00–0x1F and 0x7F) as well as all whitespace. RFC 9112 §2.2 only asks servers to ignore empty CRLF lines preceding the request-line — a carefully scoped robustness allowance intended to handle HTTP/1.0 POST workarounds. Silently absorbing NUL bytes, SOH, STX, and other non-CRLF control characters goes significantly beyond this, and can be exploited for request-boundary confusion in pipelined or multiplexed transports where a front-end component treats those bytes differently.

Affected Code

File Lines Role
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
codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java 1307–1313 SKIP_CONTROL_CHARS_BYTES ByteProcessor — skips the entire set
codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java 1275–1289 LineParser.skipControlChars — advances readerIndex past all matching bytes

Specification Analysis

RFC 9112 §2.2 — Message Parsing

In the interest of robustness, a server that is expecting to receive and parse a request-line SHOULD ignore at least one empty line (CRLF) received prior to the request-line.

An HTTP/1.1 user agent MUST NOT preface or follow a request with an extra CRLF.

Deviation

The RFC names a single permitted exception: an empty line (bare CRLF, i.e. the two-byte sequence \r\n). The ISO_CONTROL_OR_WHITESPACE table is initialised as:

for (byte b = Byte.MIN_VALUE; b < Byte.MAX_VALUE; b++) {
    ISO_CONTROL_OR_WHITESPACE[128 + b] =
        Character.isISOControl(b) || isWhitespace(b);
}

Character.isISOControl returns true for 0x000x1F and 0x7F. This includes NUL (0x00), SOH (0x01), STX (0x02), BEL (0x07), DEL (0x7F), and every other non-CRLF control character. The SKIP_CONTROL_CHARS state runs this scan unconditionally before the first READ_INITIAL, meaning any sequence of such bytes prepended to a request is silently consumed.

A load balancer or TLS terminator that does not perform the same scan sees a different message boundary than Netty does, which is the basis of a request-desync / smuggling attack.

Suggested Unit Test

Add to HttpRequestDecoderTest.java.

@Test
public void testNonCrlfControlBytesPrecedingRequestLineAreRejected() {
    // RFC 9112 §2.2: servers SHOULD ignore "at least one empty line (CRLF)" before the
    // request-line.  Non-CRLF control bytes are not part of this robustness allowance
    // and must not be silently swallowed.
    EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder());

    ByteBuf buf = Unpooled.buffer();
    buf.writeByte(0x00);   // NUL  — not an empty CRLF line
    buf.writeByte(0x01);   // SOH  — not an empty CRLF line
    buf.writeCharSequence(
            "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n",
            CharsetUtil.US_ASCII);

    channel.writeInbound(buf);
    HttpRequest req = channel.readInbound();

    // Current behaviour: NUL and SOH are in ISO_CONTROL_OR_WHITESPACE, so they are
    // silently skipped; the request decodes successfully and isFailure() == false.
    //
    // RFC-correct behaviour: only empty CRLF lines should be ignored; NUL/SOH must
    // cause a parse error — isFailure() == true.
    assertTrue(
            req.decoderResult().isFailure(),
            "Non-CRLF control bytes before the request-line must not be silently skipped " +
            "(RFC 9112 §2.2 allows only empty CRLF lines)");

    assertFalse(channel.finish());
}

Current behaviour (unfixed): skipControlChars advances past 0x00 and 0x01 because both are in ISO_CONTROL_OR_WHITESPACE; the request parses normally, isFailure() is false → test fails.

Expected behaviour after fix: only CRLF empty lines are tolerated; non-CRLF control bytes produce an error, isFailure() is true → test passes.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 4.2.14.Final"
      },
      "package": {
        "ecosystem": "Maven",
        "name": "io.netty:netty-codec-http"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "4.2.0.Final"
            },
            {
              "fixed": "4.2.15.Final"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 4.1.134.Final"
      },
      "package": {
        "ecosystem": "Maven",
        "name": "io.netty:netty-codec-http"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "4.1.135.Final"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-50020"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-444"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-06-15T20:46:36Z",
    "nvd_published_at": "2026-06-12T16:16:31Z",
    "severity": "MODERATE"
  },
  "details": "## Summary\n\nBefore reading the first request-line, `HttpObjectDecoder` skips every byte for which\n`Character.isISOControl(b)` is `true` (0x00\u20130x1F and 0x7F) as well as all whitespace.\nRFC 9112 \u00a72.2 only asks servers to ignore **empty CRLF lines** preceding the request-line \u2014\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\u20131313 | `ISO_CONTROL_OR_WHITESPACE` static initialiser \u2014 marks all ISO control chars |\n| `codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java` | 1307\u20131313 | `SKIP_CONTROL_CHARS_BYTES` `ByteProcessor` \u2014 skips the entire set |\n| `codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java` | 1275\u20131289 | `LineParser.skipControlChars` \u2014 advances `readerIndex` past all matching bytes |\n\n## Specification Analysis\n\n### RFC 9112 \u00a72.2 \u2014 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`\u2013`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 \u00a72.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  \u2014 not an empty CRLF line\n    buf.writeByte(0x01);   // SOH  \u2014 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 \u2014 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 \u00a72.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` \u2192 test **fails**.\n\n**Expected behaviour after fix:** only CRLF empty lines are tolerated; non-CRLF control\nbytes produce an error, `isFailure()` is `true` \u2192 test **passes**.",
  "id": "GHSA-hvcg-qmg6-jm4c",
  "modified": "2026-06-15T20:46:36Z",
  "published": "2026-06-15T20:46:36Z",
  "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"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Netty: HttpObjectDecoder skips arbitrary initial control characters when only initial CRLF characters are permitted"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…