Go version

go version go1.24.4 linux/amd64

Output of go env in your module/workspace:

AR='ar'
CC='gcc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='g++'
GCCGO='gccgo'
GO111MODULE='auto'
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/home/oof/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/home/oof/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build1348274841=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD=''
GOMODCACHE='/home/oof/.asdf/installs/golang/1.24.4/packages/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/oof/.asdf/installs/golang/1.24.4/packages'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/home/oof/.asdf/installs/golang/1.24.4/go'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/home/oof/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/home/oof/.asdf/installs/golang/1.24.4/go/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.24.4'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

This issue was originally reported as a security issue, but was later deemed infeasable and I was told to open a public issue here. ("If you find a scenario where this actually leads into a vulnerability in one of our products, then please get back to us, but otherwise feel free to publicly disclose this issue on GitHub as a public issue. Thanks again for your report and time, The Google Bug Hunter Team")

This program:

package main

import (
    "bytes"
    "fmt"
    "io"
    "mime/multipart"
    "net/textproto"
    // "os"
)

// headerBody is a simple helper struct for comparing parts.
type headerBodyNew struct {
    header textproto.MIMEHeader
    body   string
}

// partsFromReader reads all parts from a multipart.Reader into a slice of headerBody structs.
func partsFromReaderNew(r *multipart.Reader) ([]headerBodyNew, error) {
    var got []headerBodyNew
    for {
        p, err := r.NextPart()
        if err == io.EOF {
            return got, nil
        }
        if err != nil {
            return nil, fmt.Errorf("NextPart: %v", err)
        }
        pbody, err := io.ReadAll(p)
        if err != nil {
            return nil, fmt.Errorf("error reading part: %v", err)
        }
        got = append(got, headerBodyNew{p.Header, string(pbody)})
    }
}

// main runs the standalone fuzz-like logic on sample inputs.
func main() {
    // Define test payload
    seeds := [][]byte{
        []byte("--boundary\nContent-Transfer-Encoding:quoted-printable\n \n\n=44=41=4E=47=45=52=4F=55=53 \n--boundary--"), // POC payload...
    }

    for idx, data := range seeds {
        fmt.Printf("\n==== Testing Seed #%d ====\n", idx)
        processInput(data)
    }
}

// processInput performs parsing and round-trip logic on a given input
func processInput(data []byte) {
    const fuzzBoundary = "boundary"

    // 1. Parse the input with a Reader.
    r := multipart.NewReader(bytes.NewReader(data), fuzzBoundary)
    parts, err := partsFromReaderNew(r)
    if err != nil {
        fmt.Printf("Skipping input: parsing error: %v\n", err)
        return
    }

    // 2. Write the parts back using a Writer.
    var buf bytes.Buffer
    w := multipart.NewWriter(&buf)
    if err := w.SetBoundary(fuzzBoundary); err != nil {
        fmt.Printf("SetBoundary error: %v\n", err)
        return
    }
    for _, p := range parts {
        pw, err := w.CreatePart(p.header)
        if err != nil {
            fmt.Printf("CreatePart error: %v\n", err)
            return
        }
        if _, err := pw.Write([]byte(p.body)); err != nil {
            fmt.Printf("Write error: %v\n", err)
            return
        }
    }
    if err := w.Close(); err != nil {
        fmt.Printf("Writer.Close error: %v\n", err)
        return
    }

    // 3. Read the round-tripped data with a new Reader.
    r2 := multipart.NewReader(&buf, fuzzBoundary)
    roundtripParts, err := partsFromReaderNew(r2)
    if err != nil {
        fmt.Printf("Roundtrip parsing error: %v\n", err)
        return
    }

    // 4. Compare the original parts to the roundtripped ones.
    for i := range parts {
        gotHeader := roundtripParts[i].header
        wantHeader := parts[i].header

        fmt.Printf("\nPart #%d:\n", i)
        fmt.Println("Original Body:")
        fmt.Println(parts[i].body)
        fmt.Println("Roundtrip Body:")
        fmt.Println(roundtripParts[i].body)

        if len(gotHeader) > len(wantHeader) {
            fmt.Printf("⚠️ Header injection detected in part %d:\nGot headers: %v\nWant headers: %v\n", i, gotHeader, wantHeader)
        }
        if roundtripParts[i].body != parts[i].body {
            fmt.Printf("⚠️ Body mismatch in part %d:\nGot: %q\nWant: %q\n", i, roundtripParts[i].body, parts[i].body)
        }
    }
}

produces this result:

==== Testing Seed #0 ====

Part #0:
Original Body:
=44=41=4E=47=45=52=4F=55=53 
Roundtrip Body:
DANGEROUS
⚠️ Body mismatch in part 0:
Got: "DANGEROUS"
Want: "=44=41=4E=47=45=52=4F=55=53 "

This is because of the special treatment of the Content-Transfer-Encoding header.

What did you see happen?

The new content differs from the original content.

What did you expect to see?

The new content should be the exact same as the original.

Comment From: cherrymui

cc @neild

Comment From: seankhliao

This looks working as intended. The data represented is the same, just in different encodings. The original has Content-Transfer-Encoding: quoted-printable, the version Writer produces by default doesn't (the header is stripped, and the content is decoded).