aboutsummaryrefslogtreecommitdiff
path: root/src/crypto
diff options
context:
space:
mode:
authorNicholas S. Husin <nsh@golang.org>2025-11-24 14:56:23 -0500
committerRoland Shoemaker <roland@golang.org>2025-12-02 12:22:04 -0800
commitc1acdcb34560b20291cf55c988d0f883a38e8bbf (patch)
tree599cb3d093d24fdd8d22f5e49b1dc6bc01f50c2a /src/crypto
parent8ae5d408ed62d234cb72adebb9a23e08da1cedc6 (diff)
downloadgo-c1acdcb34560b20291cf55c988d0f883a38e8bbf.tar.xz
crypto/x509: prevent HostnameError.Error() from consuming excessive resource
Constructing HostnameError.Error() takes O(N^2) runtime due to using a string concatenation in a loop. Additionally, there is no limit on how many names are included in the error message. As a result, a malicious attacker could craft a certificate with an infinite amount of names to unfairly consume resource. To remediate this, we will now use strings.Builder to construct the error message, preventing O(N^2) runtime. When a certificate has 100 or more names, we will also not print each name individually. Thanks to Philippe Antoine (Catena cyber) for reporting this issue. Fixes #76445 Fixes CVE-2025-61729 Change-Id: I6343776ec3289577abc76dad71766c491c1a7c81 Reviewed-on: https://go-internal-review.googlesource.com/c/go/+/3000 Reviewed-by: Neal Patel <nealpatel@google.com> Reviewed-by: Damien Neil <dneil@google.com> Reviewed-on: https://go-review.googlesource.com/c/go/+/725920 Reviewed-by: Roland Shoemaker <roland@golang.org> Reviewed-by: Dmitri Shuralyov <dmitshur@google.com> TryBot-Bypass: Dmitri Shuralyov <dmitshur@golang.org>
Diffstat (limited to 'src/crypto')
-rw-r--r--src/crypto/x509/verify.go21
-rw-r--r--src/crypto/x509/verify_test.go47
2 files changed, 61 insertions, 7 deletions
diff --git a/src/crypto/x509/verify.go b/src/crypto/x509/verify.go
index fa46e226e3..1301390bee 100644
--- a/src/crypto/x509/verify.go
+++ b/src/crypto/x509/verify.go
@@ -108,31 +108,38 @@ type HostnameError struct {
func (h HostnameError) Error() string {
c := h.Certificate
+ maxNamesIncluded := 100
if !c.hasSANExtension() && matchHostnames(c.Subject.CommonName, h.Host) {
return "x509: certificate relies on legacy Common Name field, use SANs instead"
}
- var valid string
+ var valid strings.Builder
if ip := net.ParseIP(h.Host); ip != nil {
// Trying to validate an IP
if len(c.IPAddresses) == 0 {
return "x509: cannot validate certificate for " + h.Host + " because it doesn't contain any IP SANs"
}
+ if len(c.IPAddresses) >= maxNamesIncluded {
+ return fmt.Sprintf("x509: certificate is valid for %d IP SANs, but none matched %s", len(c.IPAddresses), h.Host)
+ }
for _, san := range c.IPAddresses {
- if len(valid) > 0 {
- valid += ", "
+ if valid.Len() > 0 {
+ valid.WriteString(", ")
}
- valid += san.String()
+ valid.WriteString(san.String())
}
} else {
- valid = strings.Join(c.DNSNames, ", ")
+ if len(c.DNSNames) >= maxNamesIncluded {
+ return fmt.Sprintf("x509: certificate is valid for %d names, but none matched %s", len(c.DNSNames), h.Host)
+ }
+ valid.WriteString(strings.Join(c.DNSNames, ", "))
}
- if len(valid) == 0 {
+ if valid.Len() == 0 {
return "x509: certificate is not valid for any names, but wanted to match " + h.Host
}
- return "x509: certificate is valid for " + valid + ", not " + h.Host
+ return "x509: certificate is valid for " + valid.String() + ", not " + h.Host
}
// UnknownAuthorityError results when the certificate issuer is unknown
diff --git a/src/crypto/x509/verify_test.go b/src/crypto/x509/verify_test.go
index 6dadba3a56..f9bd313737 100644
--- a/src/crypto/x509/verify_test.go
+++ b/src/crypto/x509/verify_test.go
@@ -10,13 +10,16 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
+ "crypto/rsa"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"errors"
"fmt"
"internal/testenv"
+ "log"
"math/big"
+ "net"
"os"
"os/exec"
"runtime"
@@ -90,6 +93,26 @@ var verifyTests = []verifyTest{
errorCallback: expectHostnameError("certificate is valid for"),
},
{
+ name: "TooManyDNS",
+ leaf: generatePEMCertWithRepeatSAN(1677615892, 200, "fake.dns"),
+ roots: []string{generatePEMCertWithRepeatSAN(1677615892, 200, "fake.dns")},
+ currentTime: 1677615892,
+ dnsName: "www.example.com",
+ systemSkip: true, // does not chain to a system root
+
+ errorCallback: expectHostnameError("certificate is valid for 200 names, but none matched"),
+ },
+ {
+ name: "TooManyIPs",
+ leaf: generatePEMCertWithRepeatSAN(1677615892, 150, "4.3.2.1"),
+ roots: []string{generatePEMCertWithRepeatSAN(1677615892, 150, "4.3.2.1")},
+ currentTime: 1677615892,
+ dnsName: "1.2.3.4",
+ systemSkip: true, // does not chain to a system root
+
+ errorCallback: expectHostnameError("certificate is valid for 150 IP SANs, but none matched"),
+ },
+ {
name: "IPMissing",
leaf: googleLeaf,
intermediates: []string{gtsIntermediate},
@@ -552,6 +575,30 @@ func nameToKey(name *pkix.Name) string {
return strings.Join(name.Country, ",") + "/" + strings.Join(name.Organization, ",") + "/" + strings.Join(name.OrganizationalUnit, ",") + "/" + name.CommonName
}
+func generatePEMCertWithRepeatSAN(currentTime int64, count int, san string) string {
+ cert := Certificate{
+ NotBefore: time.Unix(currentTime, 0),
+ NotAfter: time.Unix(currentTime, 0),
+ }
+ if ip := net.ParseIP(san); ip != nil {
+ cert.IPAddresses = slices.Repeat([]net.IP{ip}, count)
+ } else {
+ cert.DNSNames = slices.Repeat([]string{san}, count)
+ }
+ privKey, err := rsa.GenerateKey(rand.Reader, 4096)
+ if err != nil {
+ log.Fatal(err)
+ }
+ certBytes, err := CreateCertificate(rand.Reader, &cert, &cert, &privKey.PublicKey, privKey)
+ if err != nil {
+ log.Fatal(err)
+ }
+ return string(pem.EncodeToMemory(&pem.Block{
+ Type: "CERTIFICATE",
+ Bytes: certBytes,
+ }))
+}
+
const gtsIntermediate = `-----BEGIN CERTIFICATE-----
MIIFljCCA36gAwIBAgINAgO8U1lrNMcY9QFQZjANBgkqhkiG9w0BAQsFADBHMQsw
CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU