From 74d4d836b91318a8764b94bc2b4b66ff599eb5f2 Mon Sep 17 00:00:00 2001
From: Roland Shoemaker <bracewell@google.com>
Date: Tue, 30 Sep 2025 11:16:56 -0700
Subject: [PATCH] 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
For #75676
Fixes #75708

Change-Id: I813c4f63e78bca4054226c53e13865c781564ccf
Reviewed-on: https://go-internal-review.googlesource.com/c/go/+/2921
Reviewed-by: Nicholas Husin <husin@google.com>
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-on: https://go-internal-review.googlesource.com/c/go/+/2986
Reviewed-on: https://go-review.googlesource.com/c/go/+/709842
TryBot-Bypass: Michael Pratt <mpratt@google.com>
Auto-Submit: Michael Pratt <mpratt@google.com>
Reviewed-by: Carlos Amedee <carlos@golang.org>

CVE: CVE-2025-61723

Upstream-Status: Backport [https://github.com/golang/go/commit/74d4d836b91318a8764b94bc2b4b66ff599eb5f2]

Signed-off-by: Archana Polampalli <archana.polampalli@windriver.com>
---
 src/encoding/pem/pem.go      | 67 +++++++++++++++++++-----------------
 src/encoding/pem/pem_test.go | 13 +++----
 2 files changed, 43 insertions(+), 37 deletions(-)

diff --git a/src/encoding/pem/pem.go b/src/encoding/pem/pem.go
index 1bee1c1..01bed75 100644
--- a/src/encoding/pem/pem.go
+++ b/src/encoding/pem/pem.go
@@ -35,7 +35,7 @@ type Block struct {
 // line bytes. The remainder of the byte array (also not including the new line
 // bytes) is also returned and this will always be smaller than the original
 // argument.
-func getLine(data []byte) (line, rest []byte) {
+func getLine(data []byte) (line, rest []byte, consumed int) {
 	i := bytes.IndexByte(data, '\n')
 	var j int
 	if i < 0 {
@@ -47,7 +47,7 @@ func getLine(data []byte) (line, rest []byte) {
 			i--
 		}
 	}
-	return bytes.TrimRight(data[0:i], " \t"), data[j:]
+	return bytes.TrimRight(data[0:i], " \t"), data[j:], j
 }
 
 // removeSpacesAndTabs returns a copy of its input with all spaces and tabs
@@ -88,19 +88,29 @@ func Decode(data []byte) (p *Block, rest []byte) {
 	// the byte array, we'll accept the start string without it.
 	rest = data
 	for {
-		if bytes.HasPrefix(rest, pemStart[1:]) {
-			rest = rest[len(pemStart)-1:]
-		} else if i := bytes.Index(rest, pemStart); i >= 0 {
-			rest = rest[i+len(pemStart) : len(rest)]
-		} else {
+		// Find the first END line, and then find the last BEGIN line before
+		// the end line. This lets us skip any repeated BEGIN lines that don't
+		// have a matching END.
+		endIndex := bytes.Index(rest, pemEnd)
+		if endIndex < 0 {
 			return nil, data
 		}
-
+		endTrailerIndex := endIndex + len(pemEnd)
+		beginIndex := bytes.LastIndex(rest[:endIndex], pemStart[1:])
+		if beginIndex < 0 || beginIndex > 0 && rest[beginIndex-1] != '\n' {
+			return nil, data
+		}
+		rest = rest[beginIndex+len(pemStart)-1:]
+		endIndex -= beginIndex + len(pemStart) - 1
+		endTrailerIndex -= beginIndex + len(pemStart) - 1
 		var typeLine []byte
-		typeLine, rest = getLine(rest)
+		var consumed int
+		typeLine, rest, consumed = getLine(rest)
 		if !bytes.HasSuffix(typeLine, pemEndOfLine) {
 			continue
 		}
+		endIndex -= consumed
+		endTrailerIndex -= consumed
 		typeLine = typeLine[0 : len(typeLine)-len(pemEndOfLine)]
 
 		p = &Block{
@@ -114,7 +124,7 @@ func Decode(data []byte) (p *Block, rest []byte) {
 			if len(rest) == 0 {
 				return nil, data
 			}
-			line, next := getLine(rest)
+			line, next, consumed := getLine(rest)
 
 			i := bytes.IndexByte(line, ':')
 			if i == -1 {
@@ -127,21 +137,13 @@ func Decode(data []byte) (p *Block, rest []byte) {
 			val = bytes.TrimSpace(val)
 			p.Headers[string(key)] = string(val)
 			rest = next
+			endIndex -= consumed
+			endTrailerIndex -= consumed
 		}
 
-		var endIndex, endTrailerIndex int
-
-		// If there were no headers, the END line might occur
-		// immediately, without a leading newline.
-		if len(p.Headers) == 0 && bytes.HasPrefix(rest, pemEnd[1:]) {
-			endIndex = 0
-			endTrailerIndex = len(pemEnd) - 1
-		} else {
-			endIndex = bytes.Index(rest, pemEnd)
-			endTrailerIndex = endIndex + len(pemEnd)
-		}
-
-		if endIndex < 0 {
+		// If there were headers, there must be a newline between the headers
+		// and the END line, so endIndex should be >= 0.
+		if len(p.Headers) > 0 && endIndex < 0 {
 			continue
 		}
 
@@ -161,21 +163,24 @@ func Decode(data []byte) (p *Block, rest []byte) {
 		}
 
 		// The line must end with only whitespace.
-		if s, _ := getLine(restOfEndLine); len(s) != 0 {
+		if s, _, _ := getLine(restOfEndLine); len(s) != 0 {
 			continue
 		}
 
-		base64Data := removeSpacesAndTabs(rest[:endIndex])
-		p.Bytes = make([]byte, base64.StdEncoding.DecodedLen(len(base64Data)))
-		n, err := base64.StdEncoding.Decode(p.Bytes, base64Data)
-		if err != nil {
-			continue
+		p.Bytes = []byte{}
+		if endIndex > 0 {
+			base64Data := removeSpacesAndTabs(rest[:endIndex])
+			p.Bytes = make([]byte, base64.StdEncoding.DecodedLen(len(base64Data)))
+			n, err := base64.StdEncoding.Decode(p.Bytes, base64Data)
+			if err != nil {
+				continue
+			}
+			p.Bytes = p.Bytes[:n]
 		}
-		p.Bytes = p.Bytes[:n]
 
 		// the -1 is because we might have only matched pemEnd without the
 		// leading newline if the PEM block was empty.
-		_, rest = getLine(rest[endIndex+len(pemEnd)-1:])
+		_, rest, _ = getLine(rest[endIndex+len(pemEnd)-1:])
 		return p, rest
 	}
 }
diff --git a/src/encoding/pem/pem_test.go b/src/encoding/pem/pem_test.go
index c94b5ca..a326f9b 100644
--- a/src/encoding/pem/pem_test.go
+++ b/src/encoding/pem/pem_test.go
@@ -34,7 +34,7 @@ var getLineTests = []GetLineTest{
 
 func TestGetLine(t *testing.T) {
 	for i, test := range getLineTests {
-		x, y := getLine([]byte(test.in))
+		x, y, _ := getLine([]byte(test.in))
 		if string(x) != test.out1 || string(y) != test.out2 {
 			t.Errorf("#%d got:%+v,%+v want:%s,%s", i, x, y, test.out1, test.out2)
 		}
@@ -46,6 +46,7 @@ func TestDecode(t *testing.T) {
 	if !reflect.DeepEqual(result, certificate) {
 		t.Errorf("#0 got:%#v want:%#v", result, certificate)
 	}
+
 	result, remainder = Decode(remainder)
 	if !reflect.DeepEqual(result, privateKey) {
 		t.Errorf("#1 got:%#v want:%#v", result, privateKey)
@@ -68,7 +69,7 @@ func TestDecode(t *testing.T) {
 	}
 
 	result, remainder = Decode(remainder)
-	if result == nil || result.Type != "HEADERS" || len(result.Headers) != 1 {
+	if result == nil || result.Type != "VALID HEADERS" || len(result.Headers) != 1 {
 		t.Errorf("#5 expected single header block but got :%v", result)
 	}
 
@@ -381,15 +382,15 @@ ZWAaUoVtWIQ52aKS0p19G99hhb+IVANC4akkdHV4SP8i7MVNZhfUmg==
 
 # This shouldn't be recognised because of the missing newline after the
 headers.
------BEGIN HEADERS-----
+-----BEGIN INVALID HEADERS-----
 Header: 1
------END HEADERS-----
+-----END INVALID HEADERS-----
 
 # This should be valid, however.
------BEGIN HEADERS-----
+-----BEGIN VALID HEADERS-----
 Header: 1
 
------END HEADERS-----`)
+-----END VALID HEADERS-----`)
 
 var certificate = &Block{Type: "CERTIFICATE",
 	Headers: map[string]string{},
-- 
2.40.0

