mirror of
https://github.com/golang/go.git
synced 2025-12-08 06:10:04 +00:00
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 #75676 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-review.googlesource.com/c/go/+/709858 TryBot-Bypass: Michael Pratt <mpratt@google.com> Auto-Submit: Michael Pratt <mpratt@google.com> Reviewed-by: Carlos Amedee <carlos@golang.org>
This commit is contained in:
parent
f6f4e8b3ef
commit
5ce8cd16f3
2 changed files with 44 additions and 36 deletions
|
|
@ -37,7 +37,7 @@ type Block struct {
|
||||||
// line bytes. The remainder of the byte array (also not including the new line
|
// 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
|
// bytes) is also returned and this will always be smaller than the original
|
||||||
// argument.
|
// argument.
|
||||||
func getLine(data []byte) (line, rest []byte) {
|
func getLine(data []byte) (line, rest []byte, consumed int) {
|
||||||
i := bytes.IndexByte(data, '\n')
|
i := bytes.IndexByte(data, '\n')
|
||||||
var j int
|
var j int
|
||||||
if i < 0 {
|
if i < 0 {
|
||||||
|
|
@ -49,7 +49,7 @@ func getLine(data []byte) (line, rest []byte) {
|
||||||
i--
|
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
|
// removeSpacesAndTabs returns a copy of its input with all spaces and tabs
|
||||||
|
|
@ -90,20 +90,32 @@ func Decode(data []byte) (p *Block, rest []byte) {
|
||||||
// pemStart begins with a newline. However, at the very beginning of
|
// pemStart begins with a newline. However, at the very beginning of
|
||||||
// the byte array, we'll accept the start string without it.
|
// the byte array, we'll accept the start string without it.
|
||||||
rest = data
|
rest = data
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if bytes.HasPrefix(rest, pemStart[1:]) {
|
// Find the first END line, and then find the last BEGIN line before
|
||||||
rest = rest[len(pemStart)-1:]
|
// the end line. This lets us skip any repeated BEGIN lines that don't
|
||||||
} else if _, after, ok := bytes.Cut(rest, pemStart); ok {
|
// have a matching END.
|
||||||
rest = after
|
endIndex := bytes.Index(rest, pemEnd)
|
||||||
} else {
|
if endIndex < 0 {
|
||||||
return nil, data
|
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
|
var typeLine []byte
|
||||||
typeLine, rest = getLine(rest)
|
var consumed int
|
||||||
|
typeLine, rest, consumed = getLine(rest)
|
||||||
if !bytes.HasSuffix(typeLine, pemEndOfLine) {
|
if !bytes.HasSuffix(typeLine, pemEndOfLine) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
endIndex -= consumed
|
||||||
|
endTrailerIndex -= consumed
|
||||||
typeLine = typeLine[0 : len(typeLine)-len(pemEndOfLine)]
|
typeLine = typeLine[0 : len(typeLine)-len(pemEndOfLine)]
|
||||||
|
|
||||||
p = &Block{
|
p = &Block{
|
||||||
|
|
@ -117,7 +129,7 @@ func Decode(data []byte) (p *Block, rest []byte) {
|
||||||
if len(rest) == 0 {
|
if len(rest) == 0 {
|
||||||
return nil, data
|
return nil, data
|
||||||
}
|
}
|
||||||
line, next := getLine(rest)
|
line, next, consumed := getLine(rest)
|
||||||
|
|
||||||
key, val, ok := bytes.Cut(line, colon)
|
key, val, ok := bytes.Cut(line, colon)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
@ -129,21 +141,13 @@ func Decode(data []byte) (p *Block, rest []byte) {
|
||||||
val = bytes.TrimSpace(val)
|
val = bytes.TrimSpace(val)
|
||||||
p.Headers[string(key)] = string(val)
|
p.Headers[string(key)] = string(val)
|
||||||
rest = next
|
rest = next
|
||||||
|
endIndex -= consumed
|
||||||
|
endTrailerIndex -= consumed
|
||||||
}
|
}
|
||||||
|
|
||||||
var endIndex, endTrailerIndex int
|
// If there were headers, there must be a newline between the headers
|
||||||
|
// and the END line, so endIndex should be >= 0.
|
||||||
// If there were no headers, the END line might occur
|
if len(p.Headers) > 0 && endIndex < 0 {
|
||||||
// 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 {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,21 +167,24 @@ func Decode(data []byte) (p *Block, rest []byte) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// The line must end with only whitespace.
|
// The line must end with only whitespace.
|
||||||
if s, _ := getLine(restOfEndLine); len(s) != 0 {
|
if s, _, _ := getLine(restOfEndLine); len(s) != 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
base64Data := removeSpacesAndTabs(rest[:endIndex])
|
p.Bytes = []byte{}
|
||||||
p.Bytes = make([]byte, base64.StdEncoding.DecodedLen(len(base64Data)))
|
if endIndex > 0 {
|
||||||
n, err := base64.StdEncoding.Decode(p.Bytes, base64Data)
|
base64Data := removeSpacesAndTabs(rest[:endIndex])
|
||||||
if err != nil {
|
p.Bytes = make([]byte, base64.StdEncoding.DecodedLen(len(base64Data)))
|
||||||
continue
|
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
|
// the -1 is because we might have only matched pemEnd without the
|
||||||
// leading newline if the PEM block was empty.
|
// 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
|
return p, rest
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ var getLineTests = []GetLineTest{
|
||||||
|
|
||||||
func TestGetLine(t *testing.T) {
|
func TestGetLine(t *testing.T) {
|
||||||
for i, test := range getLineTests {
|
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 {
|
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)
|
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) {
|
if !reflect.DeepEqual(result, certificate) {
|
||||||
t.Errorf("#0 got:%#v want:%#v", result, certificate)
|
t.Errorf("#0 got:%#v want:%#v", result, certificate)
|
||||||
}
|
}
|
||||||
|
|
||||||
result, remainder = Decode(remainder)
|
result, remainder = Decode(remainder)
|
||||||
if !reflect.DeepEqual(result, privateKey) {
|
if !reflect.DeepEqual(result, privateKey) {
|
||||||
t.Errorf("#1 got:%#v want:%#v", result, privateKey)
|
t.Errorf("#1 got:%#v want:%#v", result, privateKey)
|
||||||
|
|
@ -68,7 +69,7 @@ func TestDecode(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
result, remainder = Decode(remainder)
|
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)
|
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
|
# This shouldn't be recognised because of the missing newline after the
|
||||||
headers.
|
headers.
|
||||||
-----BEGIN HEADERS-----
|
-----BEGIN INVALID HEADERS-----
|
||||||
Header: 1
|
Header: 1
|
||||||
-----END HEADERS-----
|
-----END INVALID HEADERS-----
|
||||||
|
|
||||||
# This should be valid, however.
|
# This should be valid, however.
|
||||||
-----BEGIN HEADERS-----
|
-----BEGIN VALID HEADERS-----
|
||||||
Header: 1
|
Header: 1
|
||||||
|
|
||||||
-----END HEADERS-----`)
|
-----END VALID HEADERS-----`)
|
||||||
|
|
||||||
var certificate = &Block{Type: "CERTIFICATE",
|
var certificate = &Block{Type: "CERTIFICATE",
|
||||||
Headers: map[string]string{},
|
Headers: map[string]string{},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue