Skip to content

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

@odeke-em

Description

@odeke-em

Proposal Details

Suggestion

This issue proposes adding the method Reader.SetRejectContentTransferEncoding(v bool) which if set, serves the purpose of rejecting multipart form parts that contain the "Content-Transfer-Encoding" header for binary data, including HTTP and that serves to fix security issue #63855

Motivation

To fix net/http security bug #63855, for multipart requests whose content is streamed and not yet ready until one invokes req.MultipartReader() or req.ParseMultipartForm(MAX), it is inefficient and non-elegant to trawl the bytes being streamed in. Instead it is much better to a method on the Reader which will be set by the net/http caller.

// SetRejectContentTransferEncoding toggles whether to decode parts if
// the "Content-Transfer-Encoding" header is present or not.
// If set to true, trying to decode a part will return an error if that header is present.
func (r *Reader) SetRejectContentTransferEncoding(v bool) {
        r.rejectContentTransferEncoding = v
}

Rationale

Internet Engineering Task Force (IETF) RFC 7578 circa 2015 is the current/prevailing advice for parsing multipart/form-data and it obsoletes the prior standing RFC 2388 from 1998.
RFC 7578 Section 4.7 recommends that we should reject multipart/form-data carrying the header "Content-Transfer-Encoding" and it mentions that currently no deployments supporting that legacy configuration have been discovered.
IMG_1097

Action

I have CLs (CL 573195 and 573196) available to smoothly experiment and show how this would appear ergonomically.

@neild who was advised by the bug reporters Qi Wang and Jianjun Chen, mentions that not rejecting such rejects can serve as a content smuggling vector and the current state of affairs would allow this code to print out "Joe owes €100." per
https://go.dev/play/p/wG5hidF_Uhh or inlined below

package main

import (
	"io"
	"net/http"
	"strings"
)

func main() {
	req := &http.Request{
		Header: http.Header{"Content-Type": {`multipart/form-data; boundary=xxx`}},
		Body: io.NopCloser(strings.NewReader(`--xxx
Content-Disposition: form-data; name="file"
Content-Type: text/plain
content-transfer-encoding: quoted-printable

Joe owes =E2=82=AC100
--xxx--`)),
	}

	mr, err := req.MultipartReader()
	if err != nil {
		panic(err)
	}

	part, err := mr.NextPart()
	if err != nil {
		panic(err)
	}
	defer part.Close()
	blob, err := io.ReadAll(part)
	if err != nil {
		panic(err)
	}
	println(string(blob))
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Status

    Incoming

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions