Skip to content

Commit 40c3f86

Browse files
committed
Move header size restriction from textproto to message
It's not really useful to individually check the line length or the number of fields, what we really care about is the total header size. This is also what net/http checks for. Remove size checks from textproto, since callers can implement them. Use a variant of io.LimitedReader in message instead.
1 parent 5a87ff4 commit 40c3f86

File tree

4 files changed

+49
-64
lines changed

4 files changed

+49
-64
lines changed

entity.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package message
22

33
import (
44
"bufio"
5+
"errors"
56
"io"
7+
"math"
68
"strings"
79

810
"github.com/emersion/go-message/textproto"
@@ -77,6 +79,28 @@ func NewMultipart(header Header, parts []*Entity) (*Entity, error) {
7779
return New(header, r)
7880
}
7981

82+
const maxHeaderBytes = 1 << 20 // 1 MB
83+
84+
var errHeaderTooBig = errors.New("message: header exceeds maximum size")
85+
86+
// limitedReader is the same as io.LimitedReader, but returns a custom error.
87+
type limitedReader struct {
88+
R io.Reader
89+
N int64
90+
}
91+
92+
func (lr *limitedReader) Read(p []byte) (int, error) {
93+
if lr.N <= 0 {
94+
return 0, errHeaderTooBig
95+
}
96+
if int64(len(p)) > lr.N {
97+
p = p[0:lr.N]
98+
}
99+
n, err := lr.R.Read(p)
100+
lr.N -= int64(n)
101+
return n, err
102+
}
103+
80104
// Read reads a message from r. The message's encoding and charset are
81105
// automatically decoded to raw UTF-8. Note that this function only reads the
82106
// message header.
@@ -85,12 +109,16 @@ func NewMultipart(header Header, parts []*Entity) (*Entity, error) {
85109
// error that verifies IsUnknownCharset or IsUnknownEncoding, but also returns
86110
// an Entity that can be read.
87111
func Read(r io.Reader) (*Entity, error) {
88-
br := bufio.NewReader(r)
112+
lr := &limitedReader{R: r, N: maxHeaderBytes}
113+
br := bufio.NewReader(lr)
114+
89115
h, err := textproto.ReadHeader(br)
90116
if err != nil {
91117
return nil, err
92118
}
93119

120+
lr.N = math.MaxInt64
121+
94122
return New(Header{h}, br)
95123
}
96124

entity_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,16 @@ func TestRead_single(t *testing.T) {
152152
}
153153
}
154154

155+
func TestRead_tooBig(t *testing.T) {
156+
raw := "Subject: " + strings.Repeat("A", 4096 * 1024) + "\r\n" +
157+
"\r\n" +
158+
"This header is too big.\r\n"
159+
_, err := Read(strings.NewReader(raw))
160+
if err != errHeaderTooBig {
161+
t.Fatalf("Read() = %q, want %q", err, errHeaderTooBig)
162+
}
163+
}
164+
155165
func TestEntity_WriteTo_decode(t *testing.T) {
156166
e := testMakeEntity()
157167

textproto/header.go

Lines changed: 10 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -367,16 +367,6 @@ func (h *Header) FieldsByKey(k string) HeaderFields {
367367
return &headerFieldsByKey{h, textproto.CanonicalMIMEHeaderKey(k), -1}
368368
}
369369

370-
// TooBigError is returned by ReadHeader if one of header components are larger
371-
// than allowed.
372-
type TooBigError struct {
373-
desc string
374-
}
375-
376-
func (err TooBigError) Error() string {
377-
return "textproto: length limit exceeded: " + err.desc
378-
}
379-
380370
func readLineSlice(r *bufio.Reader, line []byte) ([]byte, error) {
381371
for {
382372
l, more, err := r.ReadLine()
@@ -385,10 +375,6 @@ func readLineSlice(r *bufio.Reader, line []byte) ([]byte, error) {
385375
return line, err
386376
}
387377

388-
if len(line) > maxLineOctets {
389-
return line, TooBigError{"line"}
390-
}
391-
392378
if !more {
393379
break
394380
}
@@ -429,24 +415,19 @@ func hasContinuationLine(r *bufio.Reader) bool {
429415
return isSpace(c)
430416
}
431417

432-
func readContinuedLineSlice(r *bufio.Reader, maxLines int) (int, []byte, error) {
418+
func readContinuedLineSlice(r *bufio.Reader) ([]byte, error) {
433419
// Read the first line. We preallocate slice that it enough
434420
// for most fields.
435421
line, err := readLineSlice(r, make([]byte, 0, 256))
436422
if err == io.EOF && len(line) == 0 {
437423
// Header without a body
438-
return 0, nil, nil
424+
return nil, nil
439425
} else if err != nil {
440-
return 0, nil, err
441-
}
442-
443-
maxLines--
444-
if maxLines <= 0 {
445-
return 0, nil, TooBigError{"lines"}
426+
return nil, err
446427
}
447428

448429
if len(line) == 0 { // blank line - no continuation
449-
return maxLines, line, nil
430+
return line, nil
450431
}
451432

452433
line = append(line, '\r', '\n')
@@ -458,15 +439,10 @@ func readContinuedLineSlice(r *bufio.Reader, maxLines int) (int, []byte, error)
458439
break // bufio will keep err until next read.
459440
}
460441

461-
maxLines--
462-
if maxLines <= 0 {
463-
return 0, nil, TooBigError{"lines"}
464-
}
465-
466442
line = append(line, '\r', '\n')
467443
}
468444

469-
return maxLines, line, nil
445+
return line, nil
470446
}
471447

472448
func writeContinued(b *strings.Builder, l []byte) {
@@ -501,13 +477,12 @@ func trimAroundNewlines(v []byte) string {
501477
return b.String()
502478
}
503479

504-
const (
505-
maxHeaderLines = 1000
506-
maxLineOctets = 4000
507-
)
508-
509480
// ReadHeader reads a MIME header from r. The header is a sequence of possibly
510481
// continued Key: Value lines ending in a blank line.
482+
//
483+
// To avoid denial of service attacks, the provided bufio.Reader should be
484+
// reading from an io.LimitedReader or a similar Reader to bound the size of
485+
// headers.
511486
func ReadHeader(r *bufio.Reader) (Header, error) {
512487
fs := make([]*headerField, 0, 32)
513488

@@ -521,14 +496,12 @@ func ReadHeader(r *bufio.Reader) (Header, error) {
521496
return newHeader(fs), fmt.Errorf("message: malformed MIME header initial line: %v", string(line))
522497
}
523498

524-
maxLines := maxHeaderLines
525-
526499
for {
527500
var (
528501
kv []byte
529502
err error
530503
)
531-
maxLines, kv, err = readContinuedLineSlice(r, maxLines)
504+
kv, err = readContinuedLineSlice(r)
532505
if len(kv) == 0 {
533506
return newHeader(fs), err
534507
}

textproto/header_test.go

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -230,32 +230,6 @@ func TestInvalidHeader(t *testing.T) {
230230
}
231231
}
232232

233-
func TestReadHeader_TooBig(t *testing.T) {
234-
testHeader := "Received: from example.com by example.org\r\n" +
235-
"Received: from localhost by example.com\r\n" +
236-
"To: Taki Tachibana <taki.tachibana@example.org> " + strings.Repeat("A", 4000) + "\r\n" +
237-
"From: Mitsuha Miyamizu <mitsuha.miyamizu@example.com>\r\n\r\n"
238-
_, err := ReadHeader(bufio.NewReader(strings.NewReader(testHeader)))
239-
if err == nil {
240-
t.Fatalf("ReadHeader() succeeded")
241-
}
242-
if _, ok := err.(TooBigError); !ok {
243-
t.Fatalf("Not TooBigError returned: %T", err)
244-
}
245-
246-
testHeader = "Received: from example.com by example.org\r\n" +
247-
"Received: from localhost by example.com\r\n" +
248-
"To: Taki Tachibana <taki.tachibana@example.org>\r\n" +
249-
strings.Repeat("From: Mitsuha Miyamizu <mitsuha.miyamizu@example.com>\r\n", 1001) + "\r\n"
250-
_, err = ReadHeader(bufio.NewReader(strings.NewReader(testHeader)))
251-
if err == nil {
252-
t.Fatalf("ReadHeader() succeeded")
253-
}
254-
if _, ok := err.(TooBigError); !ok {
255-
t.Fatalf("Not TooBigError returned: %T", err)
256-
}
257-
}
258-
259233
const testHeaderWithoutBody = "Received: from example.com by example.org\r\n" +
260234
"Received: from localhost by example.com\r\n" +
261235
"To: Taki Tachibana <taki.tachibana@example.org>\r\n" +

0 commit comments

Comments
 (0)