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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
|
From 15e01a2e43ecb8c7e15ff7e9d62fe3f10dcac931 Mon Sep 17 00:00:00 2001
From: Damien Neil <dneil@google.com>
Date: Wed, 26 Feb 2025 13:40:00 -0800
Subject: [PATCH] [release-branch.go1.23] net/http: reject newlines in
chunk-size lines
Unlike request headers, where we are allowed to leniently accept
a bare LF in place of a CRLF, chunked bodies must always use CRLF
line terminators. We were already enforcing this for chunk-data lines;
do so for chunk-size lines as well. Also reject bare CRs anywhere
other than as part of the CRLF terminator.
Fixes CVE-2025-22871
Fixes #72010
For #71988
Change-Id: Ib0e21af5a8ba28c2a1ca52b72af8e2265ec79e4a
Reviewed-on: https://go-review.googlesource.com/c/go/+/652998
Reviewed-by: Jonathan Amsterdam <jba@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
(cherry picked from commit d31c805535f3fde95646ee4d87636aaaea66847b)
Reviewed-on: https://go-review.googlesource.com/c/go/+/657216
Upstream-Status: Backport [https://github.com/golang/go/commit/15e01a2e43ecb8c7e15ff7e9d62fe3f10dcac931]
CVE: CVE-2025-22871
Signed-off-by: Hitendra Prajapati <hprajapati@mvista.com>
---
src/net/http/internal/chunked.go | 19 +++++++++--
src/net/http/internal/chunked_test.go | 27 +++++++++++++++
src/net/http/serve_test.go | 49 +++++++++++++++++++++++++++
3 files changed, 92 insertions(+), 3 deletions(-)
diff --git a/src/net/http/internal/chunked.go b/src/net/http/internal/chunked.go
index ddbaacb..dd79afc 100644
--- a/src/net/http/internal/chunked.go
+++ b/src/net/http/internal/chunked.go
@@ -159,6 +159,19 @@ func readChunkLine(b *bufio.Reader) ([]byte, error) {
}
return nil, err
}
+
+ // RFC 9112 permits parsers to accept a bare \n as a line ending in headers,
+ // but not in chunked encoding lines. See https://www.rfc-editor.org/errata/eid7633,
+ // which explicitly rejects a clarification permitting \n as a chunk terminator.
+ //
+ // Verify that the line ends in a CRLF, and that no CRs appear before the end.
+ if idx := bytes.IndexByte(p, '\r'); idx == -1 {
+ return nil, errors.New("chunked line ends with bare LF")
+ } else if idx != len(p)-2 {
+ return nil, errors.New("invalid CR in chunked line")
+ }
+ p = p[:len(p)-2] // trim CRLF
+
if len(p) >= maxLineLength {
return nil, ErrLineTooLong
}
@@ -166,14 +179,14 @@ func readChunkLine(b *bufio.Reader) ([]byte, error) {
}
func trimTrailingWhitespace(b []byte) []byte {
- for len(b) > 0 && isASCIISpace(b[len(b)-1]) {
+ for len(b) > 0 && isOWS(b[len(b)-1]) {
b = b[:len(b)-1]
}
return b
}
-func isASCIISpace(b byte) bool {
- return b == ' ' || b == '\t' || b == '\n' || b == '\r'
+func isOWS(b byte) bool {
+ return b == ' ' || b == '\t'
}
// removeChunkExtension removes any chunk-extension from p.
diff --git a/src/net/http/internal/chunked_test.go b/src/net/http/internal/chunked_test.go
index 5fbeb08..51ecd62 100644
--- a/src/net/http/internal/chunked_test.go
+++ b/src/net/http/internal/chunked_test.go
@@ -251,6 +251,33 @@ func TestChunkReaderByteAtATime(t *testing.T) {
}
}
+func TestChunkInvalidInputs(t *testing.T) {
+ for _, test := range []struct {
+ name string
+ b string
+ }{{
+ name: "bare LF in chunk size",
+ b: "1\na\r\n0\r\n",
+ }, {
+ name: "extra LF in chunk size",
+ b: "1\r\r\na\r\n0\r\n",
+ }, {
+ name: "bare LF in chunk data",
+ b: "1\r\na\n0\r\n",
+ }, {
+ name: "bare LF in chunk extension",
+ b: "1;\na\r\n0\r\n",
+ }} {
+ t.Run(test.name, func(t *testing.T) {
+ r := NewChunkedReader(strings.NewReader(test.b))
+ got, err := io.ReadAll(r)
+ if err == nil {
+ t.Fatalf("unexpectedly parsed invalid chunked data:\n%q", got)
+ }
+ })
+ }
+}
+
type funcReader struct {
f func(iteration int) ([]byte, error)
i int
diff --git a/src/net/http/serve_test.go b/src/net/http/serve_test.go
index bfac783..944cd46 100644
--- a/src/net/http/serve_test.go
+++ b/src/net/http/serve_test.go
@@ -6610,3 +6610,52 @@ func testQuerySemicolon(t *testing.T, query string, wantX string, allowSemicolon
}
}
}
+
+func TestInvalidChunkedBodies(t *testing.T) {
+ for _, test := range []struct {
+ name string
+ b string
+ }{{
+ name: "bare LF in chunk size",
+ b: "1\na\r\n0\r\n\r\n",
+ }, {
+ name: "bare LF at body end",
+ b: "1\r\na\r\n0\r\n\n",
+ }} {
+ t.Run(test.name, func(t *testing.T) {
+ reqc := make(chan error)
+ ts := newClientServerTest(t, http1Mode, HandlerFunc(func(w ResponseWriter, r *Request) {
+ got, err := io.ReadAll(r.Body)
+ if err == nil {
+ t.Logf("read body: %q", got)
+ }
+ reqc <- err
+ })).ts
+
+ serverURL, err := url.Parse(ts.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ conn, err := net.Dial("tcp", serverURL.Host)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if _, err := conn.Write([]byte(
+ "POST / HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Connection: close\r\n" +
+ "\r\n" +
+ test.b)); err != nil {
+ t.Fatal(err)
+ }
+ conn.(*net.TCPConn).CloseWrite()
+
+ if err := <-reqc; err == nil {
+ t.Errorf("server handler: io.ReadAll(r.Body) succeeded, want error")
+ }
+ })
+ }
+}
--
2.25.1
|