CVE-2019-11939 (Go, published 2020-03-18T01:15Z)
Description: value: Golang Facebook Thrift servers would not error upon receiving messages declaring containers of sizes larger than the payload. As a result, malicious clients could send short messages which would result in a large memory allocation, potentially leading to denial of service. This issue affects Facebook Thrift prior to v2020.03.16.00.
Severity: MEDIUM (CVSS v3 base score: 7.5)
Weakness: CWE-770 \u2014 Allocation of Resources Without Limits or Throttling
The product allocates a reusable resource or group of resources on behalf of an actor without imposing any restrictions on the size or number of resources that can be allocated, in violation of the intended security policy for that actor.
Repository: https://github.com/facebook/fbthrift
Files changed:
thrift/lib/go/thrift/binary_protocol.go
thrift/lib/go/thrift/binary_protocol_test.go
thrift/lib/go/thrift/compact_protocol.go
thrift/lib/go/thrift/compact_protocol_test.go
thrift/lib/go/thrift/http_client.go
thrift/lib/go/thrift/protocol_test.go
Commit message: Validate container size against total message size

Summary:
For performance reasons we preallocate golang containers with
the size given in the field header.  This allowed an attacker to
trigger very large memory allocations and potentially crash the server
with small messages.  Before creating the golang container confirm
that the message is theoretically large enough to contain a list/map/set
of the given size.

This requires that the binary and compact protocols use transports
that can expose the amount of data waiting to be read.  As a result of
this change you will not be able to do things like use the raw socket
transport or talk to endpoints over the HTTP transport that don't send
a content length header.

Fixes CVE-2019-11939.

Differential Revision: D19595758

fbshipit-source-id: 48bb9dbaf0467cea7a54602f0b07b00a8755c3f9


Vulnerable code:
	if uint64(size) > p.trans.RemainingBytes() {
	if uint64(size) > p.trans.RemainingBytes() {
	// skip over a map with invalid key/value type and 1.7B entries
	data := []byte("\n\x10\rO\t6\x03\n\n\n\x10\r\n\tslice\x00")
	err := d.Read(&m, data)

	if uint64(length) > p.trans.RemainingBytes() {
	if uint64(length) > p.trans.RemainingBytes() {
		NewStreamTransportRW(bytes.NewBuffer(make([]byte, 0, 16384))),
	"io/ioutil"
	var err error
	if p.response != nil && p.response.Body != nil {
		// The docs specify that if keepalive is enabled and the response body is not
		// read to completion the connection will never be returned to the pool and
		// reused. Errors are being ignored here because if the connection is invalid
		// and this fails for some reason, the Close() method will do any remaining
		// cleanup.
		io.Copy(ioutil.Discard, p.response.Body)

		err = p.response.Body.Close()
	}

	return err
	n, err := p.response.Body.Read(buf)
	return readByte(p.response.Body)
	len := p.response.ContentLength
	if len >= 0 {
		return uint64(len)
	}

	return UnknownRemaining // the truth is, we just don't know unless framed is used
	"bytes"
		NewStreamTransportFactory(rdr, writer, false),                             // use a pipe
		NewStreamTransportFactory(rConn, wConn, false),                            // use tcp over network
	buf := bytes.NewBuffer(make([]byte, 0, 1024))
		NewStreamTransportFactory(buf, buf, true),

Fixed code:
	if uint64(size32*2) > p.trans.RemainingBytes() || p.trans.RemainingBytes() == UnknownRemaining {
		err = invalidDataLength
		return
	}
	if uint64(size32) > p.trans.RemainingBytes() || p.trans.RemainingBytes() == UnknownRemaining {
		err = invalidDataLength
		return
	}
	if uint64(size32) > p.trans.RemainingBytes() || p.trans.RemainingBytes() == UnknownRemaining {
		err = invalidDataLength
		return
	}
	if uint64(size) > p.trans.RemainingBytes() || p.trans.RemainingBytes() == UnknownRemaining {
	if uint64(size) > p.trans.RemainingBytes() || p.trans.RemainingBytes() == UnknownRemaining {
	"bytes"
	"fmt"
	// skip over a map with invalid key/value type and ~550M entries
	data := make([]byte, 1100000000)
	copy(data[:], []byte("\n\x10\rO\t6\x03\n\n\n\x10\r\n\tsl ce\x00"))
	transport, _ := d.Transport.(*MemoryBuffer)
	transport.Buffer = bytes.NewBuffer(data)
	err := m.Read(d.Protocol)

func TestInitialAllocationMapBinaryProtocol(t *testing.T) {
	var m MyTestStruct
	d := NewDeserializer()
	f := NewBinaryProtocolFactoryDefault()
	d.Protocol = f.GetProtocol(d.Transport)
	// attempts to allocate a map with 1.8B elements for a 20 byte message
	data := []byte("\n\x10\rO\t6\x03\n\n\n\x10\r\n\tslice\x00")
	err := d.Read(&m, data)
	if err == nil {
		t.Fatalf("Parsed invalid message correctly")
	} else if !strings.Contains(err.Error(), "Invalid data length") {
		t.Fatalf("Failed for reason besides Invalid data length")
	}
}

func TestInitialAllocationListBinaryProtocol(t *testing.T) {
	var m MyTestStruct
	d := NewDeserializer()
	f := NewBinaryProtocolFactoryDefault()
	d.Protocol = f.GetProtocol(d.Transport)
	// attempts to allocate a list with 1.8B elements for a 20 byte message
	data := []byte("\n\x10\rO\t6\x03\n\n\n\x10\x0f\n\tslice\x00")
	err := d.Read(&m, data)
	if err == nil {
		t.Fatalf("Parsed invalid message correctly")
	} else if !strings.Contains(err.Error(), "Invalid data length") {
		t.Fatalf("Failed for reason besides Invalid data length")
	}
}

func TestInitialAllocationSetBinaryProtocol(t *testing.T) {
	var m MyTestStruct
	d := NewDeserializer()
	f := NewBinaryProtocolFactoryDefault()
	d.Protocol = f.GetProtocol(d.Transport)
	// attempts to allocate a set with 1.8B elements for a 20 byte message
	data := []byte("\n\x12\rO\t6\x03\n\n\n\x10\x0e\n\tslice\x00")
	err := d.Read(&m, data)
	if err == nil {
		t.Fatalf("Parsed invalid message correctly")
	} else if !strings.Contains(err.Error(), "Invalid data length") {
		fmt.Printf("Got %+v", err)
		t.Fatalf("Failed for reason besides Invalid data length")
	}
}
	if uint64(size32*2) > p.trans.RemainingBytes() || p.trans.RemainingBytes() == UnknownRemaining {
		err = invalidDataLength
		return
	}
	if uint64(size) > p.trans.RemainingBytes() || p.trans.RemainingBytes() == UnknownRemaining {
		err = invalidDataLength
		return
	}

	if uint64(length) > p.trans.RemainingBytes() || p.trans.RemainingBytes() == UnknownRemaining {
	if uint64(length) > p.trans.RemainingBytes() || p.trans.RemainingBytes() == UnknownRemaining {
	"strings"

func TestInitialAllocationMapCompactProtocol(t *testing.T) {
	var m MyTestStruct
	d := NewDeserializer()
	f := NewCompactProtocolFactory()
	d.Protocol = f.GetProtocol(d.Transport)
	// attempts to allocate a map of 930M elements for a 9 byte message
	data := []byte("%0\x88\x8a\x97\xb7\xc4\x030")
	err := d.Read(&m, data)
	if err == nil {
		t.Fatalf("Parsed invalid message correctly")
	} else if !strings.Contains(err.Error(), "Invalid data length") {
		t.Fatalf("Failed for reason besides Invalid data length")
	}
}

func TestInitialAllocationListCompactProtocol(t *testing.T) {
	var m MyTestStruct
	d := NewDeserializer()
	f := NewCompactProtocolFactory()
	d.Protocol = f.GetProtocol(d.Transport)
	// attempts to allocate a list of 950M elements for an 11 byte message
	data := []byte("%0\x98\xfa\xb7\xb7\xc4\xc4\x03\x01a")
	err := d.Read(&m, data)
	if err == nil {
		t.Fatalf("Parsed invalid message correctly")
	} else if !strings.Contains(err.Error(), "Invalid data length") {
		t.Fatalf("Failed for reason besides Invalid data length")
	}
}

func TestInitialAllocationSetCompactProtocol(t *testing.T) {
	var m MyTestStruct
	d := NewDeserializer()
	f := NewCompactProtocolFactory()
	d.Protocol = f.GetProtocol(d.Transport)
	// attempts to allocate a list of 950M elements for an 11 byte message
	data := []byte("%0\xa8\xfa\x97\xb7\xc4\xc4\x03\x01a")
	err := d.Read(&m, data)
	if err == nil {
		t.Fatalf("Parsed invalid message correctly")
	} else if !strings.Contains(err.Error(), "Invalid data length") {
		t.Fatalf("Failed for reason besides Invalid data length")
	}
}

func TestInitialAllocationMapCompactProtocolLimitedR(t *testing.T) {
	var m MyTestStruct

	// attempts to allocate a map of 930M elements for a 9 byte message
	data := []byte("%0\x88\x8a\x97\xb7\xc4\x030")
	p := NewCompactProtocol(
		NewStreamTransportLimitedR(bytes.NewBuffer(data), len(data)),
	)

	err := m.Read(p)
	if err == nil {
		t.Fatalf("Parsed invalid message correctly")
	} else if !strings.Contains(err.Error(), "Invalid data length") {
		t.Fatalf("Failed for reason besides Invalid data length")
	}
}
	responseBuffer     bytes.Buffer
	p.responseBuffer.Reset()
	return nil
	n, err := p.responseBuffer.Read(buf)
	return readByte(&p.responseBuffer)

	defer response.Body.Close()


	_, err = io.Copy(&p.responseBuffer, response.Body)
	if err != nil {
		return NewTransportExceptionFromError(err)
	}

	return uint64(p.responseBuffer.Len())