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
- proposal: mime/multipart: add method Reader.SetRejectContentTransferEncoding(bool) to support net/http rejecting multipart form parts with "Content-Transfer-Encoding" header which is explicitly deprecated by RFC 7578 Section 4.7 #66434
- mime/multipart: Failure to round trip leading to potential bypass of content based checks #74087 (closed)
- proposal: mime/multipart: allow specifying content type in Writer.CreateFormFile #49329 (closed)
- mime: bypassing multi-boundary check via Parameter Value Continuations #47602
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 ?