Skip to content

Commit c1acdcb

Browse files
nicholashusinrolandshoemaker
authored andcommitted
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 <[email protected]> Reviewed-by: Damien Neil <[email protected]> Reviewed-on: https://go-review.googlesource.com/c/go/+/725920 Reviewed-by: Roland Shoemaker <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]> TryBot-Bypass: Dmitri Shuralyov <[email protected]>
1 parent 8ae5d40 commit c1acdcb

File tree

2 files changed

+61
-7
lines changed

2 files changed

+61
-7
lines changed

src/crypto/x509/verify.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,31 +108,38 @@ type HostnameError struct {
108108

109109
func (h HostnameError) Error() string {
110110
c := h.Certificate
111+
maxNamesIncluded := 100
111112

112113
if !c.hasSANExtension() && matchHostnames(c.Subject.CommonName, h.Host) {
113114
return "x509: certificate relies on legacy Common Name field, use SANs instead"
114115
}
115116

116-
var valid string
117+
var valid strings.Builder
117118
if ip := net.ParseIP(h.Host); ip != nil {
118119
// Trying to validate an IP
119120
if len(c.IPAddresses) == 0 {
120121
return "x509: cannot validate certificate for " + h.Host + " because it doesn't contain any IP SANs"
121122
}
123+
if len(c.IPAddresses) >= maxNamesIncluded {
124+
return fmt.Sprintf("x509: certificate is valid for %d IP SANs, but none matched %s", len(c.IPAddresses), h.Host)
125+
}
122126
for _, san := range c.IPAddresses {
123-
if len(valid) > 0 {
124-
valid += ", "
127+
if valid.Len() > 0 {
128+
valid.WriteString(", ")
125129
}
126-
valid += san.String()
130+
valid.WriteString(san.String())
127131
}
128132
} else {
129-
valid = strings.Join(c.DNSNames, ", ")
133+
if len(c.DNSNames) >= maxNamesIncluded {
134+
return fmt.Sprintf("x509: certificate is valid for %d names, but none matched %s", len(c.DNSNames), h.Host)
135+
}
136+
valid.WriteString(strings.Join(c.DNSNames, ", "))
130137
}
131138

132-
if len(valid) == 0 {
139+
if valid.Len() == 0 {
133140
return "x509: certificate is not valid for any names, but wanted to match " + h.Host
134141
}
135-
return "x509: certificate is valid for " + valid + ", not " + h.Host
142+
return "x509: certificate is valid for " + valid.String() + ", not " + h.Host
136143
}
137144

138145
// UnknownAuthorityError results when the certificate issuer is unknown

src/crypto/x509/verify_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@ import (
1010
"crypto/ecdsa"
1111
"crypto/elliptic"
1212
"crypto/rand"
13+
"crypto/rsa"
1314
"crypto/x509/pkix"
1415
"encoding/asn1"
1516
"encoding/pem"
1617
"errors"
1718
"fmt"
1819
"internal/testenv"
20+
"log"
1921
"math/big"
22+
"net"
2023
"os"
2124
"os/exec"
2225
"runtime"
@@ -89,6 +92,26 @@ var verifyTests = []verifyTest{
8992

9093
errorCallback: expectHostnameError("certificate is valid for"),
9194
},
95+
{
96+
name: "TooManyDNS",
97+
leaf: generatePEMCertWithRepeatSAN(1677615892, 200, "fake.dns"),
98+
roots: []string{generatePEMCertWithRepeatSAN(1677615892, 200, "fake.dns")},
99+
currentTime: 1677615892,
100+
dnsName: "www.example.com",
101+
systemSkip: true, // does not chain to a system root
102+
103+
errorCallback: expectHostnameError("certificate is valid for 200 names, but none matched"),
104+
},
105+
{
106+
name: "TooManyIPs",
107+
leaf: generatePEMCertWithRepeatSAN(1677615892, 150, "4.3.2.1"),
108+
roots: []string{generatePEMCertWithRepeatSAN(1677615892, 150, "4.3.2.1")},
109+
currentTime: 1677615892,
110+
dnsName: "1.2.3.4",
111+
systemSkip: true, // does not chain to a system root
112+
113+
errorCallback: expectHostnameError("certificate is valid for 150 IP SANs, but none matched"),
114+
},
92115
{
93116
name: "IPMissing",
94117
leaf: googleLeaf,
@@ -552,6 +575,30 @@ func nameToKey(name *pkix.Name) string {
552575
return strings.Join(name.Country, ",") + "/" + strings.Join(name.Organization, ",") + "/" + strings.Join(name.OrganizationalUnit, ",") + "/" + name.CommonName
553576
}
554577

578+
func generatePEMCertWithRepeatSAN(currentTime int64, count int, san string) string {
579+
cert := Certificate{
580+
NotBefore: time.Unix(currentTime, 0),
581+
NotAfter: time.Unix(currentTime, 0),
582+
}
583+
if ip := net.ParseIP(san); ip != nil {
584+
cert.IPAddresses = slices.Repeat([]net.IP{ip}, count)
585+
} else {
586+
cert.DNSNames = slices.Repeat([]string{san}, count)
587+
}
588+
privKey, err := rsa.GenerateKey(rand.Reader, 4096)
589+
if err != nil {
590+
log.Fatal(err)
591+
}
592+
certBytes, err := CreateCertificate(rand.Reader, &cert, &cert, &privKey.PublicKey, privKey)
593+
if err != nil {
594+
log.Fatal(err)
595+
}
596+
return string(pem.EncodeToMemory(&pem.Block{
597+
Type: "CERTIFICATE",
598+
Bytes: certBytes,
599+
}))
600+
}
601+
555602
const gtsIntermediate = `-----BEGIN CERTIFICATE-----
556603
MIIFljCCA36gAwIBAgINAgO8U1lrNMcY9QFQZjANBgkqhkiG9w0BAQsFADBHMQsw
557604
CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU

0 commit comments

Comments
 (0)