Skip to content

Commit 5ce8cd1

Browse files
rolandshoemakergopherbot
authored andcommitted
encoding/pem: make Decode complexity linear
Because Decode scanned the input first for the first BEGIN line, and then the first END line, the complexity of Decode is quadratic. If the input contained a large number of BEGINs and then a single END right at the end of the input, we would find the first BEGIN, and then scan the entire input for the END, and fail to parse the block, so move onto the next BEGIN, scan the entire input for the END, etc. Instead, look for the first END in the input, and then the first BEGIN that precedes the found END. We then process the bytes between the BEGIN and END, and move onto the bytes after the END for further processing. This gives us linear complexity. Fixes CVE-2025-61723 Fixes golang#75676 Change-Id: I813c4f63e78bca4054226c53e13865c781564ccf Reviewed-on: https://go-internal-review.googlesource.com/c/go/+/2921 Reviewed-by: Nicholas Husin <[email protected]> Reviewed-by: Damien Neil <[email protected]> Reviewed-on: https://go-review.googlesource.com/c/go/+/709858 TryBot-Bypass: Michael Pratt <[email protected]> Auto-Submit: Michael Pratt <[email protected]> Reviewed-by: Carlos Amedee <[email protected]>
1 parent f6f4e8b commit 5ce8cd1

File tree

2 files changed

+44
-36
lines changed

2 files changed

+44
-36
lines changed

src/encoding/pem/pem.go

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ type Block struct {
3737
// line bytes. The remainder of the byte array (also not including the new line
3838
// bytes) is also returned and this will always be smaller than the original
3939
// argument.
40-
func getLine(data []byte) (line, rest []byte) {
40+
func getLine(data []byte) (line, rest []byte, consumed int) {
4141
i := bytes.IndexByte(data, '\n')
4242
var j int
4343
if i < 0 {
@@ -49,7 +49,7 @@ func getLine(data []byte) (line, rest []byte) {
4949
i--
5050
}
5151
}
52-
return bytes.TrimRight(data[0:i], " \t"), data[j:]
52+
return bytes.TrimRight(data[0:i], " \t"), data[j:], j
5353
}
5454

5555
// removeSpacesAndTabs returns a copy of its input with all spaces and tabs
@@ -90,20 +90,32 @@ func Decode(data []byte) (p *Block, rest []byte) {
9090
// pemStart begins with a newline. However, at the very beginning of
9191
// the byte array, we'll accept the start string without it.
9292
rest = data
93+
9394
for {
94-
if bytes.HasPrefix(rest, pemStart[1:]) {
95-
rest = rest[len(pemStart)-1:]
96-
} else if _, after, ok := bytes.Cut(rest, pemStart); ok {
97-
rest = after
98-
} else {
95+
// Find the first END line, and then find the last BEGIN line before
96+
// the end line. This lets us skip any repeated BEGIN lines that don't
97+
// have a matching END.
98+
endIndex := bytes.Index(rest, pemEnd)
99+
if endIndex < 0 {
100+
return nil, data
101+
}
102+
endTrailerIndex := endIndex + len(pemEnd)
103+
beginIndex := bytes.LastIndex(rest[:endIndex], pemStart[1:])
104+
if beginIndex < 0 || beginIndex > 0 && rest[beginIndex-1] != '\n' {
99105
return nil, data
100106
}
107+
rest = rest[beginIndex+len(pemStart)-1:]
108+
endIndex -= beginIndex + len(pemStart) - 1
109+
endTrailerIndex -= beginIndex + len(pemStart) - 1
101110

102111
var typeLine []byte
103-
typeLine, rest = getLine(rest)
112+
var consumed int
113+
typeLine, rest, consumed = getLine(rest)
104114
if !bytes.HasSuffix(typeLine, pemEndOfLine) {
105115
continue
106116
}
117+
endIndex -= consumed
118+
endTrailerIndex -= consumed
107119
typeLine = typeLine[0 : len(typeLine)-len(pemEndOfLine)]
108120

109121
p = &Block{
@@ -117,7 +129,7 @@ func Decode(data []byte) (p *Block, rest []byte) {
117129
if len(rest) == 0 {
118130
return nil, data
119131
}
120-
line, next := getLine(rest)
132+
line, next, consumed := getLine(rest)
121133

122134
key, val, ok := bytes.Cut(line, colon)
123135
if !ok {
@@ -129,21 +141,13 @@ func Decode(data []byte) (p *Block, rest []byte) {
129141
val = bytes.TrimSpace(val)
130142
p.Headers[string(key)] = string(val)
131143
rest = next
144+
endIndex -= consumed
145+
endTrailerIndex -= consumed
132146
}
133147

134-
var endIndex, endTrailerIndex int
135-
136-
// If there were no headers, the END line might occur
137-
// immediately, without a leading newline.
138-
if len(p.Headers) == 0 && bytes.HasPrefix(rest, pemEnd[1:]) {
139-
endIndex = 0
140-
endTrailerIndex = len(pemEnd) - 1
141-
} else {
142-
endIndex = bytes.Index(rest, pemEnd)
143-
endTrailerIndex = endIndex + len(pemEnd)
144-
}
145-
146-
if endIndex < 0 {
148+
// If there were headers, there must be a newline between the headers
149+
// and the END line, so endIndex should be >= 0.
150+
if len(p.Headers) > 0 && endIndex < 0 {
147151
continue
148152
}
149153

@@ -163,21 +167,24 @@ func Decode(data []byte) (p *Block, rest []byte) {
163167
}
164168

165169
// The line must end with only whitespace.
166-
if s, _ := getLine(restOfEndLine); len(s) != 0 {
170+
if s, _, _ := getLine(restOfEndLine); len(s) != 0 {
167171
continue
168172
}
169173

170-
base64Data := removeSpacesAndTabs(rest[:endIndex])
171-
p.Bytes = make([]byte, base64.StdEncoding.DecodedLen(len(base64Data)))
172-
n, err := base64.StdEncoding.Decode(p.Bytes, base64Data)
173-
if err != nil {
174-
continue
174+
p.Bytes = []byte{}
175+
if endIndex > 0 {
176+
base64Data := removeSpacesAndTabs(rest[:endIndex])
177+
p.Bytes = make([]byte, base64.StdEncoding.DecodedLen(len(base64Data)))
178+
n, err := base64.StdEncoding.Decode(p.Bytes, base64Data)
179+
if err != nil {
180+
continue
181+
}
182+
p.Bytes = p.Bytes[:n]
175183
}
176-
p.Bytes = p.Bytes[:n]
177184

178185
// the -1 is because we might have only matched pemEnd without the
179186
// leading newline if the PEM block was empty.
180-
_, rest = getLine(rest[endIndex+len(pemEnd)-1:])
187+
_, rest, _ = getLine(rest[endIndex+len(pemEnd)-1:])
181188
return p, rest
182189
}
183190
}

src/encoding/pem/pem_test.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ var getLineTests = []GetLineTest{
3434

3535
func TestGetLine(t *testing.T) {
3636
for i, test := range getLineTests {
37-
x, y := getLine([]byte(test.in))
37+
x, y, _ := getLine([]byte(test.in))
3838
if string(x) != test.out1 || string(y) != test.out2 {
3939
t.Errorf("#%d got:%+v,%+v want:%s,%s", i, x, y, test.out1, test.out2)
4040
}
@@ -46,6 +46,7 @@ func TestDecode(t *testing.T) {
4646
if !reflect.DeepEqual(result, certificate) {
4747
t.Errorf("#0 got:%#v want:%#v", result, certificate)
4848
}
49+
4950
result, remainder = Decode(remainder)
5051
if !reflect.DeepEqual(result, privateKey) {
5152
t.Errorf("#1 got:%#v want:%#v", result, privateKey)
@@ -68,7 +69,7 @@ func TestDecode(t *testing.T) {
6869
}
6970

7071
result, remainder = Decode(remainder)
71-
if result == nil || result.Type != "HEADERS" || len(result.Headers) != 1 {
72+
if result == nil || result.Type != "VALID HEADERS" || len(result.Headers) != 1 {
7273
t.Errorf("#5 expected single header block but got :%v", result)
7374
}
7475

@@ -381,15 +382,15 @@ ZWAaUoVtWIQ52aKS0p19G99hhb+IVANC4akkdHV4SP8i7MVNZhfUmg==
381382
382383
# This shouldn't be recognised because of the missing newline after the
383384
headers.
384-
-----BEGIN HEADERS-----
385+
-----BEGIN INVALID HEADERS-----
385386
Header: 1
386-
-----END HEADERS-----
387+
-----END INVALID HEADERS-----
387388
388389
# This should be valid, however.
389-
-----BEGIN HEADERS-----
390+
-----BEGIN VALID HEADERS-----
390391
Header: 1
391392
392-
-----END HEADERS-----`)
393+
-----END VALID HEADERS-----`)
393394

394395
var certificate = &Block{Type: "CERTIFICATE",
395396
Headers: map[string]string{},

0 commit comments

Comments
 (0)