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.

  • Source code: https://github.com/golang/go/blob/go1.25.0/src/mime/multipart/writer.go#L128-L157

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:

  • Chrome/Firefox
  • undici (Node.js): https://github.com/nodejs/undici/blob/v7.15.0/lib/web/fetch/body.js#L128-L129
  • urllib3 (Python): https://github.com/urllib3/urllib3/blob/2.5.0/src/urllib3/fields.py#L112-L114
  • curl (with default option): https://github.com/curl/curl/blob/curl-8_15_0/lib/mime.c#L299-L310

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.

  • Related discussion (curl): https://github.com/curl/curl/issues/7789

Comment From: gabyhelp

Related Issues

Related Code Changes

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)

Comment From: seankhliao

is this a dupe of #33801 ?