1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
|
From: James Hillyerd <james@hillyerd.com>
Date: Thu, 2 Feb 2023 11:50:20 -0800
Subject: fix: Reject invalid MIME header name characters to support Go 1.20
(#270)
* Test with go 1.20 release candidate
* header: Detect invalid header name characters for Go 1.20
* header: test for permitted special chars
Origin: backport, https://github.com/jhillyerd/enmime/commit/415ff72f
---
header.go | 59 +++++++++++++++++++++++++++++++++++++++++++++++++---------
header_test.go | 18 +++++++++++++-----
2 files changed, 63 insertions(+), 14 deletions(-)
diff --git a/header.go b/header.go
index cd9d28b..628e765 100644
--- a/header.go
+++ b/header.go
@@ -81,9 +81,9 @@ var AddressHeaders = map[string]bool{
// ParseMediaType is a more tolerant implementation of Go's mime.ParseMediaType function.
//
// Tolerances accounted for:
-// * Missing ';' between content-type and media parameters
-// * Repeating media parameters
-// * Unquoted values in media parameters containing 'tspecials' characters
+// - Missing ';' between content-type and media parameters
+// - Repeating media parameters
+// - Unquoted values in media parameters containing 'tspecials' characters
func ParseMediaType(ctype string) (mtype string, params map[string]string, invalidParams []string,
err error) {
// Export of internal function.
@@ -97,6 +97,7 @@ func readHeader(r *bufio.Reader, p *Part) (textproto.MIMEHeader, error) {
buf := &bytes.Buffer{}
tp := textproto.NewReader(r)
firstHeader := true
+line:
for {
// Pull out each line of the headers as a temporary slice s
s, err := tp.ReadLineBytes()
@@ -119,18 +120,29 @@ func readHeader(r *bufio.Reader, p *Part) (textproto.MIMEHeader, error) {
continue
}
if firstColon > 0 {
- // Contains a colon, treat as a new header line
- if !firstHeader {
- // New Header line, end the previous
- buf.Write([]byte{'\r', '\n'})
- }
-
// Behavior change in net/textproto package in Golang 1.12.10 and 1.13.1:
// A space preceding the first colon in a header line is no longer handled
// automatically due to CVE-2019-16276 which takes advantage of this
// particular violation of RFC-7230 to exploit HTTP/1.1
if bytes.Contains(s[:firstColon+1], []byte{' ', ':'}) {
s = bytes.Replace(s, []byte{' ', ':'}, []byte{':'}, 1)
+ firstColon = bytes.IndexByte(s, ':')
+ }
+
+ // Behavior change in net/textproto package in Golang 1.20: invalid characters
+ // in header keys are no longer allowed; https://github.com/golang/go/issues/53188
+ for _, c := range s[:firstColon] {
+ if c != ' ' && !validHeaderFieldByte(c) {
+ p.addError(
+ ErrorMalformedHeader, "Header name %q contains invalid character %q", s, c)
+ continue line
+ }
+ }
+
+ // Contains a colon, treat as a new header line
+ if !firstHeader {
+ // New Header line, end the previous
+ buf.Write([]byte{'\r', '\n'})
}
s = textproto.TrimBytes(s)
@@ -213,3 +225,32 @@ func quotedDisplayName(s string) string {
func whiteSpaceRune(r rune) bool {
return r == ' ' || r == '\t' || r == '\r' || r == '\n'
}
+
+// Mirror of func in net/textproto/reader.go; from go 1.20.0-rc.2
+func validHeaderFieldByte(c byte) bool {
+ // mask is a 128-bit bitmap with 1s for allowed bytes,
+ // so that the byte c can be tested with a shift and an and.
+ // If c >= 128, then 1<<c and 1<<(c-64) will both be zero,
+ // and this function will return false.
+ const mask = 0 |
+ (1<<(10)-1)<<'0' |
+ (1<<(26)-1)<<'a' |
+ (1<<(26)-1)<<'A' |
+ 1<<'!' |
+ 1<<'#' |
+ 1<<'$' |
+ 1<<'%' |
+ 1<<'&' |
+ 1<<'\'' |
+ 1<<'*' |
+ 1<<'+' |
+ 1<<'-' |
+ 1<<'.' |
+ 1<<'^' |
+ 1<<'_' |
+ 1<<'`' |
+ 1<<'|' |
+ 1<<'~'
+ return ((uint64(1)<<c)&(mask&(1<<64-1)) |
+ (uint64(1)<<(c-64))&(mask>>64)) != 0
+}
diff --git a/header_test.go b/header_test.go
index 01055be..5b18568 100644
--- a/header_test.go
+++ b/header_test.go
@@ -99,8 +99,6 @@ func TestReadHeader(t *testing.T) {
{
label: "missing name",
input: ": line1=foo\r\n",
- hname: "",
- want: "",
correct: false,
},
{
@@ -127,12 +125,17 @@ func TestReadHeader(t *testing.T) {
correct: true,
},
{
- label: "equals in name",
- input: "name=value:text\n",
- hname: "name=value",
+ label: "permitted specials in name",
+ input: "Begin!#$%&'*+-.^_`|~End: text\n",
+ hname: "Begin!#$%&'*+-.^_`|~End",
want: "text",
correct: true,
},
+ {
+ label: "equals in name",
+ input: "name=value:text\n",
+ correct: false,
+ },
{
label: "no space before continuation",
input: "X-Bad-Continuation: line1=foo;\nline2=bar\n",
@@ -236,6 +239,11 @@ func TestReadHeader(t *testing.T) {
gotErrs := len(p.Errors)
if gotErrs != wantErrs {
t.Errorf("Got %v p.Errors, want %v", gotErrs, wantErrs)
+ if gotErrs > 0 {
+ for _, e := range p.Errors {
+ t.Log(e)
+ }
+ }
}
// Check for extra headers by removing expected ones.
|