From 1fb53495280817b1d709e13523bfd79aec3f1976 Mon Sep 17 00:00:00 2001 From: huzhangying Date: Thu, 20 Nov 2025 16:30:39 +0800 Subject: [PATCH] [backport]fix CVE-2025-47912,CVE-2025-58186 --- ...-net-url-enforce-stricter-parsing-of.patch | 214 +++++++ ...et-http-add-httpcookiemaxnum-GODEBUG.patch | 588 ++++++++++++++++++ golang.spec | 10 +- 3 files changed, 811 insertions(+), 1 deletion(-) create mode 100644 1018-CVE-2025-47912-net-url-enforce-stricter-parsing-of.patch create mode 100644 1019-CVE-2025-58186-net-http-add-httpcookiemaxnum-GODEBUG.patch diff --git a/1018-CVE-2025-47912-net-url-enforce-stricter-parsing-of.patch b/1018-CVE-2025-47912-net-url-enforce-stricter-parsing-of.patch new file mode 100644 index 0000000..301fc69 --- /dev/null +++ b/1018-CVE-2025-47912-net-url-enforce-stricter-parsing-of.patch @@ -0,0 +1,214 @@ +From f6f4e8b3ef21299db1ea3a343c3e55e91365a7fd Mon Sep 17 00:00:00 2001 +From: Ethan Lee +Date: Fri, 29 Aug 2025 17:35:55 +0000 +Subject: [PATCH] net/url: enforce stricter parsing of + bracketed IPv6 hostnames + +- Previously, url.Parse did not enforce validation of hostnames within + square brackets. +- RFC 3986 stipulates that only IPv6 hostnames can be embedded within + square brackets in a URL. +- Now, the parsing logic should strictly enforce that only IPv6 + hostnames can be resolved when in square brackets. IPv4, IPv4-mapped + addresses and other input will be rejected. +- Update url_test to add test cases that cover the above scenarios. + +Thanks to Enze Wang, Jingcheng Yang and Zehui Miao of Tsinghua +University for reporting this issue. + +Reference: https://go-review.googlesource.com/c/go/+/709857 +Conclict: no + +Fixes CVE-2025-47912 +Fixes #75678 + +Change-Id: Iaa41432bf0ee86de95a39a03adae5729e4deb46c +Reviewed-on: https://go-internal-review.googlesource.com/c/go/+/2680 +Reviewed-by: Damien Neil +Reviewed-by: Roland Shoemaker +Reviewed-on: https://go-review.googlesource.com/c/go/+/709857 +TryBot-Bypass: Michael Pratt +Reviewed-by: Carlos Amedee +Auto-Submit: Michael Pratt +--- + src/go/build/deps_test.go | 7 ++++++- + src/net/url/url.go | 42 +++++++++++++++++++++++++++++---------- + src/net/url/url_test.go | 39 ++++++++++++++++++++++++++++++++++++ + 3 files changed, 77 insertions(+), 11 deletions(-) + +diff --git a/src/go/build/deps_test.go b/src/go/build/deps_test.go +index 2a1606e..f6d70c0 100644 +--- a/src/go/build/deps_test.go ++++ b/src/go/build/deps_test.go +@@ -233,7 +233,6 @@ var depsRules = ` + internal/types/errors, + mime/quotedprintable, + net/internal/socktest, +- net/url, + runtime/trace, + text/scanner, + text/tabwriter; +@@ -276,6 +275,12 @@ var depsRules = ` + FMT + < text/template/parse; + ++ internal/bytealg, internal/itoa, math/bits, slices, strconv, unique ++ < net/netip; ++ ++ FMT, net/netip ++ < net/url; ++ + net/url, text/template/parse + < text/template + < internal/lazytemplate; +diff --git a/src/net/url/url.go b/src/net/url/url.go +index 8a8de1c..c686239 100644 +--- a/src/net/url/url.go ++++ b/src/net/url/url.go +@@ -14,6 +14,7 @@ import ( + "errors" + "fmt" + "maps" ++ "net/netip" + "path" + "slices" + "strconv" +@@ -623,40 +624,61 @@ func parseAuthority(authority string) (user *Userinfo, host string, err error) { + // parseHost parses host as an authority without user + // information. That is, as host[:port]. + func parseHost(host string) (string, error) { +- if strings.HasPrefix(host, "[") { ++ if openBracketIdx := strings.LastIndex(host, "["); openBracketIdx != -1 { + // Parse an IP-Literal in RFC 3986 and RFC 6874. + // E.g., "[fe80::1]", "[fe80::1%25en0]", "[fe80::1]:80". +- i := strings.LastIndex(host, "]") +- if i < 0 { ++ closeBracketIdx := strings.LastIndex(host, "]") ++ if closeBracketIdx < 0 { + return "", errors.New("missing ']' in host") + } +- colonPort := host[i+1:] ++ ++ colonPort := host[closeBracketIdx+1:] + if !validOptionalPort(colonPort) { + return "", fmt.Errorf("invalid port %q after host", colonPort) + } ++ unescapedColonPort, err := unescape(colonPort, encodeHost) ++ if err != nil { ++ return "", err ++ } + ++ hostname := host[openBracketIdx+1 : closeBracketIdx] ++ var unescapedHostname string + // RFC 6874 defines that %25 (%-encoded percent) introduces + // the zone identifier, and the zone identifier can use basically + // any %-encoding it likes. That's different from the host, which + // can only %-encode non-ASCII bytes. + // We do impose some restrictions on the zone, to avoid stupidity + // like newlines. +- zone := strings.Index(host[:i], "%25") +- if zone >= 0 { +- host1, err := unescape(host[:zone], encodeHost) ++ zoneIdx := strings.Index(hostname, "%25") ++ if zoneIdx >= 0 { ++ hostPart, err := unescape(hostname[:zoneIdx], encodeHost) + if err != nil { + return "", err + } +- host2, err := unescape(host[zone:i], encodeZone) ++ zonePart, err := unescape(hostname[zoneIdx:], encodeZone) + if err != nil { + return "", err + } +- host3, err := unescape(host[i:], encodeHost) ++ unescapedHostname = hostPart + zonePart ++ } else { ++ var err error ++ unescapedHostname, err = unescape(hostname, encodeHost) + if err != nil { + return "", err + } +- return host1 + host2 + host3, nil + } ++ ++ // Per RFC 3986, only a host identified by a valid ++ // IPv6 address can be enclosed by square brackets. ++ // This excludes any IPv4 or IPv4-mapped addresses. ++ addr, err := netip.ParseAddr(unescapedHostname) ++ if err != nil { ++ return "", fmt.Errorf("invalid host: %w", err) ++ } ++ if addr.Is4() || addr.Is4In6() { ++ return "", errors.New("invalid IPv6 host") ++ } ++ return "[" + unescapedHostname + "]" + unescapedColonPort, nil + } else if i := strings.LastIndex(host, ":"); i != -1 { + colonPort := host[i:] + if !validOptionalPort(colonPort) { +diff --git a/src/net/url/url_test.go b/src/net/url/url_test.go +index 16e08b6..3206558 100644 +--- a/src/net/url/url_test.go ++++ b/src/net/url/url_test.go +@@ -383,6 +383,16 @@ var urltests = []URLTest{ + }, + "", + }, ++ // valid IPv6 host with port and path ++ { ++ "https://[2001:db8::1]:8443/test/path", ++ &URL{ ++ Scheme: "https", ++ Host: "[2001:db8::1]:8443", ++ Path: "/test/path", ++ }, ++ "", ++ }, + // host subcomponent; IPv6 address with zone identifier in RFC 6874 + { + "http://[fe80::1%25en0]/", // alphanum zone identifier +@@ -707,6 +717,24 @@ var parseRequestURLTests = []struct { + // RFC 6874. + {"http://[fe80::1%en0]/", false}, + {"http://[fe80::1%en0]:8080/", false}, ++ ++ // Tests exercising RFC 3986 compliance ++ {"https://[1:2:3:4:5:6:7:8]", true}, // full IPv6 address ++ {"https://[2001:db8::a:b:c:d]", true}, // compressed IPv6 address ++ {"https://[fe80::1%25eth0]", true}, // link-local address with zone ID (interface name) ++ {"https://[fe80::abc:def%254]", true}, // link-local address with zone ID (interface index) ++ {"https://[2001:db8::1]/path", true}, // compressed IPv6 address with path ++ {"https://[fe80::1%25eth0]/path?query=1", true}, // link-local with zone, path, and query ++ ++ {"https://[::ffff:192.0.2.1]", false}, ++ {"https://[:1] ", false}, ++ {"https://[1:2:3:4:5:6:7:8:9]", false}, ++ {"https://[1::1::1]", false}, ++ {"https://[1:2:3:]", false}, ++ {"https://[ffff::127.0.0.4000]", false}, ++ {"https://[0:0::test.com]:80", false}, ++ {"https://[2001:db8::test.com]", false}, ++ {"https://[test.com]", false}, + } + + func TestParseRequestURI(t *testing.T) { +@@ -1643,6 +1671,17 @@ func TestParseErrors(t *testing.T) { + {"cache_object:foo", true}, + {"cache_object:foo/bar", true}, + {"cache_object/:foo/bar", false}, ++ ++ {"http://[192.168.0.1]/", true}, // IPv4 in brackets ++ {"http://[192.168.0.1]:8080/", true}, // IPv4 in brackets with port ++ {"http://[::ffff:192.168.0.1]/", true}, // IPv4-mapped IPv6 in brackets ++ {"http://[::ffff:192.168.0.1]:8080/", true}, // IPv4-mapped IPv6 in brackets with port ++ {"http://[::ffff:c0a8:1]/", true}, // IPv4-mapped IPv6 in brackets (hex) ++ {"http://[not-an-ip]/", true}, // invalid IP string in brackets ++ {"http://[fe80::1%foo]/", true}, // invalid zone format in brackets ++ {"http://[fe80::1", true}, // missing closing bracket ++ {"http://fe80::1]/", true}, // missing opening bracket ++ {"http://[test.com]/", true}, // domain name in brackets + } + for _, tt := range tests { + u, err := Parse(tt.in) +-- +2.43.0 + diff --git a/1019-CVE-2025-58186-net-http-add-httpcookiemaxnum-GODEBUG.patch b/1019-CVE-2025-58186-net-http-add-httpcookiemaxnum-GODEBUG.patch new file mode 100644 index 0000000..9d16b41 --- /dev/null +++ b/1019-CVE-2025-58186-net-http-add-httpcookiemaxnum-GODEBUG.patch @@ -0,0 +1,588 @@ +From 9b9d02c5a015910ce57024788de2ff254c6cfca6 Mon Sep 17 00:00:00 2001 +From: Nicholas Husin +Date: Tue, 30 Sep 2025 14:02:38 -0400 +Subject: [PATCH] net/http: add httpcookiemaxnum GODEBUG option + to limit number of cookies parsed + +Reference: https://go-review.googlesource.com/c/go/+/709855 +Conflict: no + +When handling HTTP headers, net/http does not currently limit the number +of cookies that can be parsed. The only limitation that exists is for +the size of the entire HTTP header, which is controlled by +MaxHeaderBytes (defaults to 1 MB). + +Unfortunately, this allows a malicious actor to send HTTP headers which +contain a massive amount of small cookies, such that as much cookies as +possible can be fitted within the MaxHeaderBytes limitation. Internally, +this causes us to allocate a massive number of Cookie struct. + +For example, a 1 MB HTTP header with cookies that repeats "a=;" will +cause an allocation of ~66 MB in the heap. This can serve as a way for +malicious actors to induce memory exhaustion. + +To fix this, we will now limit the number of cookies we are willing to +parse to 3000 by default. This behavior can be changed by setting a new +GODEBUG option: GODEBUG=httpcookiemaxnum. httpcookiemaxnum can be set to +allow a higher or lower cookie limit. Setting it to 0 will also allow an +infinite number of cookies to be parsed. + +Thanks to jub0bs for reporting this issue. + +For #75672 +Fixes CVE-2025-58186 + +Change-Id: Ied58b3bc8acf5d11c880f881f36ecbf1d5d52622 +Reviewed-on: https://go-internal-review.googlesource.com/c/go/+/2720 +Reviewed-by: Roland Shoemaker +Reviewed-by: Damien Neil +Reviewed-on: https://go-review.googlesource.com/c/go/+/709855 +Reviewed-by: Carlos Amedee +LUCI-TryBot-Result: Go LUCI +Auto-Submit: Michael Pratt +--- + doc/godebug.md | 10 ++ + src/internal/godebugs/table.go | 1 + + src/net/http/cookie.go | 59 +++++++++- + src/net/http/cookie_test.go | 206 ++++++++++++++++++++++----------- + src/runtime/metrics/doc.go | 5 + + 5 files changed, 206 insertions(+), 75 deletions(-) + +diff --git a/doc/godebug.md b/doc/godebug.md +index 5eb5018..5acf32f 100644 +--- a/doc/godebug.md ++++ b/doc/godebug.md +@@ -151,6 +151,16 @@ for example, + see the [runtime documentation](/pkg/runtime#hdr-Environment_Variables) + and the [go command documentation](/cmd/go#hdr-Build_and_test_caching). + ++### Go 1.26 ++ ++Go 1.26 added a new `httpcookiemaxnum` setting that controls the maximum number ++of cookies that net/http will accept when parsing HTTP headers. If the number of ++cookie in a header exceeds the number set in `httpcookiemaxnum`, cookie parsing ++will fail early. The default value is `httpcookiemaxnum=3000`. Setting ++`httpcookiemaxnum=0` will allow the cookie parsing to accept an indefinite ++number of cookies. To avoid denial of service attacks, this setting and default ++was backported to Go 1.25.2 and Go 1.24.8. ++ + ### Go 1.24 + + Go 1.24 added a new `fips140` setting that controls whether the Go +diff --git a/src/internal/godebugs/table.go b/src/internal/godebugs/table.go +index 9278a12..6a9bf4f 100644 +--- a/src/internal/godebugs/table.go ++++ b/src/internal/godebugs/table.go +@@ -38,6 +38,7 @@ var All = []Info{ + {Name: "http2client", Package: "net/http"}, + {Name: "http2debug", Package: "net/http", Opaque: true}, + {Name: "http2server", Package: "net/http"}, ++ {Name: "httpcookiemaxnum", Package: "net/http", Changed: 24, Old: "0"}, + {Name: "httplaxcontentlength", Package: "net/http", Changed: 22, Old: "1"}, + {Name: "httpmuxgo121", Package: "net/http", Changed: 22, Old: "1"}, + {Name: "httpservecontentkeepheaders", Package: "net/http", Changed: 23, Old: "1"}, +diff --git a/src/net/http/cookie.go b/src/net/http/cookie.go +index 3483e16..a64ff0e 100644 +--- a/src/net/http/cookie.go ++++ b/src/net/http/cookie.go +@@ -7,6 +7,7 @@ package http + import ( + "errors" + "fmt" ++ "internal/godebug" + "log" + "net" + "net/http/internal/ascii" +@@ -16,6 +17,8 @@ import ( + "time" + ) + ++var httpcookiemaxnum = godebug.New("httpcookiemaxnum") ++ + // A Cookie represents an HTTP cookie as sent in the Set-Cookie header of an + // HTTP response or the Cookie header of an HTTP request. + // +@@ -58,16 +61,37 @@ const ( + ) + + var ( +- errBlankCookie = errors.New("http: blank cookie") +- errEqualNotFoundInCookie = errors.New("http: '=' not found in cookie") +- errInvalidCookieName = errors.New("http: invalid cookie name") +- errInvalidCookieValue = errors.New("http: invalid cookie value") ++ errBlankCookie = errors.New("http: blank cookie") ++ errEqualNotFoundInCookie = errors.New("http: '=' not found in cookie") ++ errInvalidCookieName = errors.New("http: invalid cookie name") ++ errInvalidCookieValue = errors.New("http: invalid cookie value") ++ errCookieNumLimitExceeded = errors.New("http: number of cookies exceeded limit") + ) + ++const defaultCookieMaxNum = 3000 ++ ++func cookieNumWithinMax(cookieNum int) bool { ++ withinDefaultMax := cookieNum <= defaultCookieMaxNum ++ if httpcookiemaxnum.Value() == "" { ++ return withinDefaultMax ++ } ++ if customMax, err := strconv.Atoi(httpcookiemaxnum.Value()); err == nil { ++ withinCustomMax := customMax == 0 || cookieNum <= customMax ++ if withinDefaultMax != withinCustomMax { ++ httpcookiemaxnum.IncNonDefault() ++ } ++ return withinCustomMax ++ } ++ return withinDefaultMax ++} ++ + // ParseCookie parses a Cookie header value and returns all the cookies + // which were set in it. Since the same cookie name can appear multiple times + // the returned Values can contain more than one value for a given key. + func ParseCookie(line string) ([]*Cookie, error) { ++ if !cookieNumWithinMax(strings.Count(line, ";") + 1) { ++ return nil, errCookieNumLimitExceeded ++ } + parts := strings.Split(textproto.TrimString(line), ";") + if len(parts) == 1 && parts[0] == "" { + return nil, errBlankCookie +@@ -197,11 +221,21 @@ func ParseSetCookie(line string) (*Cookie, error) { + + // readSetCookies parses all "Set-Cookie" values from + // the header h and returns the successfully parsed Cookies. ++// ++// If the amount of cookies exceeds CookieNumLimit, and httpcookielimitnum ++// GODEBUG option is not explicitly turned off, this function will silently ++// fail and return an empty slice. + func readSetCookies(h Header) []*Cookie { + cookieCount := len(h["Set-Cookie"]) + if cookieCount == 0 { + return []*Cookie{} + } ++ // Cookie limit was unfortunately introduced at a later point in time. ++ // As such, we can only fail by returning an empty slice rather than ++ // explicit error. ++ if !cookieNumWithinMax(cookieCount) { ++ return []*Cookie{} ++ } + cookies := make([]*Cookie, 0, cookieCount) + for _, line := range h["Set-Cookie"] { + if cookie, err := ParseSetCookie(line); err == nil { +@@ -329,13 +363,28 @@ func (c *Cookie) Valid() error { + // readCookies parses all "Cookie" values from the header h and + // returns the successfully parsed Cookies. + // +-// if filter isn't empty, only cookies of that name are returned. ++// If filter isn't empty, only cookies of that name are returned. ++// ++// If the amount of cookies exceeds CookieNumLimit, and httpcookielimitnum ++// GODEBUG option is not explicitly turned off, this function will silently ++// fail and return an empty slice. + func readCookies(h Header, filter string) []*Cookie { + lines := h["Cookie"] + if len(lines) == 0 { + return []*Cookie{} + } + ++ // Cookie limit was unfortunately introduced at a later point in time. ++ // As such, we can only fail by returning an empty slice rather than ++ // explicit error. ++ cookieCount := 0 ++ for _, line := range lines { ++ cookieCount += strings.Count(line, ";") + 1 ++ } ++ if !cookieNumWithinMax(cookieCount) { ++ return []*Cookie{} ++ } ++ + cookies := make([]*Cookie, 0, len(lines)+strings.Count(lines[0], ";")) + for _, line := range lines { + line = textproto.TrimString(line) +diff --git a/src/net/http/cookie_test.go b/src/net/http/cookie_test.go +index aac6956..d028725 100644 +--- a/src/net/http/cookie_test.go ++++ b/src/net/http/cookie_test.go +@@ -11,6 +11,7 @@ import ( + "log" + "os" + "reflect" ++ "slices" + "strings" + "testing" + "time" +@@ -255,16 +256,17 @@ func TestAddCookie(t *testing.T) { + } + + var readSetCookiesTests = []struct { +- Header Header +- Cookies []*Cookie ++ header Header ++ cookies []*Cookie ++ godebug string + }{ + { +- Header{"Set-Cookie": {"Cookie-1=v$1"}}, +- []*Cookie{{Name: "Cookie-1", Value: "v$1", Raw: "Cookie-1=v$1"}}, ++ header: Header{"Set-Cookie": {"Cookie-1=v$1"}}, ++ cookies: []*Cookie{{Name: "Cookie-1", Value: "v$1", Raw: "Cookie-1=v$1"}}, + }, + { +- Header{"Set-Cookie": {"NID=99=YsDT5i3E-CXax-; expires=Wed, 23-Nov-2011 01:05:03 GMT; path=/; domain=.google.ch; HttpOnly"}}, +- []*Cookie{{ ++ header: Header{"Set-Cookie": {"NID=99=YsDT5i3E-CXax-; expires=Wed, 23-Nov-2011 01:05:03 GMT; path=/; domain=.google.ch; HttpOnly"}}, ++ cookies: []*Cookie{{ + Name: "NID", + Value: "99=YsDT5i3E-CXax-", + Path: "/", +@@ -276,8 +278,8 @@ var readSetCookiesTests = []struct { + }}, + }, + { +- Header{"Set-Cookie": {".ASPXAUTH=7E3AA; expires=Wed, 07-Mar-2012 14:25:06 GMT; path=/; HttpOnly"}}, +- []*Cookie{{ ++ header: Header{"Set-Cookie": {".ASPXAUTH=7E3AA; expires=Wed, 07-Mar-2012 14:25:06 GMT; path=/; HttpOnly"}}, ++ cookies: []*Cookie{{ + Name: ".ASPXAUTH", + Value: "7E3AA", + Path: "/", +@@ -288,8 +290,8 @@ var readSetCookiesTests = []struct { + }}, + }, + { +- Header{"Set-Cookie": {"ASP.NET_SessionId=foo; path=/; HttpOnly"}}, +- []*Cookie{{ ++ header: Header{"Set-Cookie": {"ASP.NET_SessionId=foo; path=/; HttpOnly"}}, ++ cookies: []*Cookie{{ + Name: "ASP.NET_SessionId", + Value: "foo", + Path: "/", +@@ -298,8 +300,8 @@ var readSetCookiesTests = []struct { + }}, + }, + { +- Header{"Set-Cookie": {"samesitedefault=foo; SameSite"}}, +- []*Cookie{{ ++ header: Header{"Set-Cookie": {"samesitedefault=foo; SameSite"}}, ++ cookies: []*Cookie{{ + Name: "samesitedefault", + Value: "foo", + SameSite: SameSiteDefaultMode, +@@ -307,8 +309,8 @@ var readSetCookiesTests = []struct { + }}, + }, + { +- Header{"Set-Cookie": {"samesiteinvalidisdefault=foo; SameSite=invalid"}}, +- []*Cookie{{ ++ header: Header{"Set-Cookie": {"samesiteinvalidisdefault=foo; SameSite=invalid"}}, ++ cookies: []*Cookie{{ + Name: "samesiteinvalidisdefault", + Value: "foo", + SameSite: SameSiteDefaultMode, +@@ -316,8 +318,8 @@ var readSetCookiesTests = []struct { + }}, + }, + { +- Header{"Set-Cookie": {"samesitelax=foo; SameSite=Lax"}}, +- []*Cookie{{ ++ header: Header{"Set-Cookie": {"samesitelax=foo; SameSite=Lax"}}, ++ cookies: []*Cookie{{ + Name: "samesitelax", + Value: "foo", + SameSite: SameSiteLaxMode, +@@ -325,8 +327,8 @@ var readSetCookiesTests = []struct { + }}, + }, + { +- Header{"Set-Cookie": {"samesitestrict=foo; SameSite=Strict"}}, +- []*Cookie{{ ++ header: Header{"Set-Cookie": {"samesitestrict=foo; SameSite=Strict"}}, ++ cookies: []*Cookie{{ + Name: "samesitestrict", + Value: "foo", + SameSite: SameSiteStrictMode, +@@ -334,8 +336,8 @@ var readSetCookiesTests = []struct { + }}, + }, + { +- Header{"Set-Cookie": {"samesitenone=foo; SameSite=None"}}, +- []*Cookie{{ ++ header: Header{"Set-Cookie": {"samesitenone=foo; SameSite=None"}}, ++ cookies: []*Cookie{{ + Name: "samesitenone", + Value: "foo", + SameSite: SameSiteNoneMode, +@@ -345,47 +347,66 @@ var readSetCookiesTests = []struct { + // Make sure we can properly read back the Set-Cookie headers we create + // for values containing spaces or commas: + { +- Header{"Set-Cookie": {`special-1=a z`}}, +- []*Cookie{{Name: "special-1", Value: "a z", Raw: `special-1=a z`}}, ++ header: Header{"Set-Cookie": {`special-1=a z`}}, ++ cookies: []*Cookie{{Name: "special-1", Value: "a z", Raw: `special-1=a z`}}, + }, + { +- Header{"Set-Cookie": {`special-2=" z"`}}, +- []*Cookie{{Name: "special-2", Value: " z", Quoted: true, Raw: `special-2=" z"`}}, ++ header: Header{"Set-Cookie": {`special-2=" z"`}}, ++ cookies: []*Cookie{{Name: "special-2", Value: " z", Quoted: true, Raw: `special-2=" z"`}}, + }, + { +- Header{"Set-Cookie": {`special-3="a "`}}, +- []*Cookie{{Name: "special-3", Value: "a ", Quoted: true, Raw: `special-3="a "`}}, ++ header: Header{"Set-Cookie": {`special-3="a "`}}, ++ cookies: []*Cookie{{Name: "special-3", Value: "a ", Quoted: true, Raw: `special-3="a "`}}, + }, + { +- Header{"Set-Cookie": {`special-4=" "`}}, +- []*Cookie{{Name: "special-4", Value: " ", Quoted: true, Raw: `special-4=" "`}}, ++ header: Header{"Set-Cookie": {`special-4=" "`}}, ++ cookies: []*Cookie{{Name: "special-4", Value: " ", Quoted: true, Raw: `special-4=" "`}}, + }, + { +- Header{"Set-Cookie": {`special-5=a,z`}}, +- []*Cookie{{Name: "special-5", Value: "a,z", Raw: `special-5=a,z`}}, ++ header: Header{"Set-Cookie": {`special-5=a,z`}}, ++ cookies: []*Cookie{{Name: "special-5", Value: "a,z", Raw: `special-5=a,z`}}, + }, + { +- Header{"Set-Cookie": {`special-6=",z"`}}, +- []*Cookie{{Name: "special-6", Value: ",z", Quoted: true, Raw: `special-6=",z"`}}, ++ header: Header{"Set-Cookie": {`special-6=",z"`}}, ++ cookies: []*Cookie{{Name: "special-6", Value: ",z", Quoted: true, Raw: `special-6=",z"`}}, + }, + { +- Header{"Set-Cookie": {`special-7=a,`}}, +- []*Cookie{{Name: "special-7", Value: "a,", Raw: `special-7=a,`}}, ++ header: Header{"Set-Cookie": {`special-7=a,`}}, ++ cookies: []*Cookie{{Name: "special-7", Value: "a,", Raw: `special-7=a,`}}, + }, + { +- Header{"Set-Cookie": {`special-8=","`}}, +- []*Cookie{{Name: "special-8", Value: ",", Quoted: true, Raw: `special-8=","`}}, ++ header: Header{"Set-Cookie": {`special-8=","`}}, ++ cookies: []*Cookie{{Name: "special-8", Value: ",", Quoted: true, Raw: `special-8=","`}}, + }, + // Make sure we can properly read back the Set-Cookie headers + // for names containing spaces: + { +- Header{"Set-Cookie": {`special-9 =","`}}, +- []*Cookie{{Name: "special-9", Value: ",", Quoted: true, Raw: `special-9 =","`}}, ++ header: Header{"Set-Cookie": {`special-9 =","`}}, ++ cookies: []*Cookie{{Name: "special-9", Value: ",", Quoted: true, Raw: `special-9 =","`}}, + }, + // Quoted values (issue #46443) + { +- Header{"Set-Cookie": {`cookie="quoted"`}}, +- []*Cookie{{Name: "cookie", Value: "quoted", Quoted: true, Raw: `cookie="quoted"`}}, ++ header: Header{"Set-Cookie": {`cookie="quoted"`}}, ++ cookies: []*Cookie{{Name: "cookie", Value: "quoted", Quoted: true, Raw: `cookie="quoted"`}}, ++ }, ++ { ++ header: Header{"Set-Cookie": slices.Repeat([]string{"a="}, defaultCookieMaxNum+1)}, ++ cookies: []*Cookie{}, ++ }, ++ { ++ header: Header{"Set-Cookie": slices.Repeat([]string{"a="}, 10)}, ++ cookies: []*Cookie{}, ++ godebug: "httpcookiemaxnum=5", ++ }, ++ { ++ header: Header{"Set-Cookie": strings.Split(strings.Repeat(";a=", defaultCookieMaxNum+1)[1:], ";")}, ++ cookies: slices.Repeat([]*Cookie{{Name: "a", Value: "", Quoted: false, Raw: "a="}}, defaultCookieMaxNum+1), ++ godebug: "httpcookiemaxnum=0", ++ }, ++ { ++ header: Header{"Set-Cookie": strings.Split(strings.Repeat(";a=", defaultCookieMaxNum+1)[1:], ";")}, ++ cookies: slices.Repeat([]*Cookie{{Name: "a", Value: "", Quoted: false, Raw: "a="}}, defaultCookieMaxNum+1), ++ godebug: fmt.Sprintf("httpcookiemaxnum=%v", defaultCookieMaxNum+1), + }, + + // TODO(bradfitz): users have reported seeing this in the +@@ -405,79 +426,103 @@ func toJSON(v any) string { + + func TestReadSetCookies(t *testing.T) { + for i, tt := range readSetCookiesTests { ++ t.Setenv("GODEBUG", tt.godebug) + for n := 0; n < 2; n++ { // to verify readSetCookies doesn't mutate its input +- c := readSetCookies(tt.Header) +- if !reflect.DeepEqual(c, tt.Cookies) { +- t.Errorf("#%d readSetCookies: have\n%s\nwant\n%s\n", i, toJSON(c), toJSON(tt.Cookies)) ++ c := readSetCookies(tt.header) ++ if !reflect.DeepEqual(c, tt.cookies) { ++ t.Errorf("#%d readSetCookies: have\n%s\nwant\n%s\n", i, toJSON(c), toJSON(tt.cookies)) + } + } + } + } + + var readCookiesTests = []struct { +- Header Header +- Filter string +- Cookies []*Cookie ++ header Header ++ filter string ++ cookies []*Cookie ++ godebug string + }{ + { +- Header{"Cookie": {"Cookie-1=v$1", "c2=v2"}}, +- "", +- []*Cookie{ ++ header: Header{"Cookie": {"Cookie-1=v$1", "c2=v2"}}, ++ filter: "", ++ cookies: []*Cookie{ + {Name: "Cookie-1", Value: "v$1"}, + {Name: "c2", Value: "v2"}, + }, + }, + { +- Header{"Cookie": {"Cookie-1=v$1", "c2=v2"}}, +- "c2", +- []*Cookie{ ++ header: Header{"Cookie": {"Cookie-1=v$1", "c2=v2"}}, ++ filter: "c2", ++ cookies: []*Cookie{ + {Name: "c2", Value: "v2"}, + }, + }, + { +- Header{"Cookie": {"Cookie-1=v$1; c2=v2"}}, +- "", +- []*Cookie{ ++ header: Header{"Cookie": {"Cookie-1=v$1; c2=v2"}}, ++ filter: "", ++ cookies: []*Cookie{ + {Name: "Cookie-1", Value: "v$1"}, + {Name: "c2", Value: "v2"}, + }, + }, + { +- Header{"Cookie": {"Cookie-1=v$1; c2=v2"}}, +- "c2", +- []*Cookie{ ++ header: Header{"Cookie": {"Cookie-1=v$1; c2=v2"}}, ++ filter: "c2", ++ cookies: []*Cookie{ + {Name: "c2", Value: "v2"}, + }, + }, + { +- Header{"Cookie": {`Cookie-1="v$1"; c2="v2"`}}, +- "", +- []*Cookie{ ++ header: Header{"Cookie": {`Cookie-1="v$1"; c2="v2"`}}, ++ filter: "", ++ cookies: []*Cookie{ + {Name: "Cookie-1", Value: "v$1", Quoted: true}, + {Name: "c2", Value: "v2", Quoted: true}, + }, + }, + { +- Header{"Cookie": {`Cookie-1="v$1"; c2=v2;`}}, +- "", +- []*Cookie{ ++ header: Header{"Cookie": {`Cookie-1="v$1"; c2=v2;`}}, ++ filter: "", ++ cookies: []*Cookie{ + {Name: "Cookie-1", Value: "v$1", Quoted: true}, + {Name: "c2", Value: "v2"}, + }, + }, + { +- Header{"Cookie": {``}}, +- "", +- []*Cookie{}, ++ header: Header{"Cookie": {``}}, ++ filter: "", ++ cookies: []*Cookie{}, ++ }, ++ // GODEBUG=httpcookiemaxnum should work regardless if all cookies are sent ++ // via one "Cookie" field, or multiple fields. ++ { ++ header: Header{"Cookie": {strings.Repeat(";a=", defaultCookieMaxNum+1)[1:]}}, ++ cookies: []*Cookie{}, ++ }, ++ { ++ header: Header{"Cookie": slices.Repeat([]string{"a="}, 10)}, ++ cookies: []*Cookie{}, ++ godebug: "httpcookiemaxnum=5", ++ }, ++ { ++ header: Header{"Cookie": {strings.Repeat(";a=", defaultCookieMaxNum+1)[1:]}}, ++ cookies: slices.Repeat([]*Cookie{{Name: "a", Value: "", Quoted: false}}, defaultCookieMaxNum+1), ++ godebug: "httpcookiemaxnum=0", ++ }, ++ { ++ header: Header{"Cookie": slices.Repeat([]string{"a="}, defaultCookieMaxNum+1)}, ++ cookies: slices.Repeat([]*Cookie{{Name: "a", Value: "", Quoted: false}}, defaultCookieMaxNum+1), ++ godebug: fmt.Sprintf("httpcookiemaxnum=%v", defaultCookieMaxNum+1), + }, + } + + func TestReadCookies(t *testing.T) { + for i, tt := range readCookiesTests { ++ t.Setenv("GODEBUG", tt.godebug) + for n := 0; n < 2; n++ { // to verify readCookies doesn't mutate its input +- c := readCookies(tt.Header, tt.Filter) +- if !reflect.DeepEqual(c, tt.Cookies) { +- t.Errorf("#%d readCookies:\nhave: %s\nwant: %s\n", i, toJSON(c), toJSON(tt.Cookies)) ++ c := readCookies(tt.header, tt.filter) ++ if !reflect.DeepEqual(c, tt.cookies) { ++ t.Errorf("#%d readCookies:\nhave: %s\nwant: %s\n", i, toJSON(c), toJSON(tt.cookies)) + } + } + } +@@ -689,6 +734,7 @@ func TestParseCookie(t *testing.T) { + line string + cookies []*Cookie + err error ++ godebug string + }{ + { + line: "Cookie-1=v$1", +@@ -722,8 +768,28 @@ func TestParseCookie(t *testing.T) { + line: "k1=\\", + err: errInvalidCookieValue, + }, ++ { ++ line: strings.Repeat(";a=", defaultCookieMaxNum+1)[1:], ++ err: errCookieNumLimitExceeded, ++ }, ++ { ++ line: strings.Repeat(";a=", 10)[1:], ++ err: errCookieNumLimitExceeded, ++ godebug: "httpcookiemaxnum=5", ++ }, ++ { ++ line: strings.Repeat(";a=", defaultCookieMaxNum+1)[1:], ++ cookies: slices.Repeat([]*Cookie{{Name: "a", Value: "", Quoted: false}}, defaultCookieMaxNum+1), ++ godebug: "httpcookiemaxnum=0", ++ }, ++ { ++ line: strings.Repeat(";a=", defaultCookieMaxNum+1)[1:], ++ cookies: slices.Repeat([]*Cookie{{Name: "a", Value: "", Quoted: false}}, defaultCookieMaxNum+1), ++ godebug: fmt.Sprintf("httpcookiemaxnum=%v", defaultCookieMaxNum+1), ++ }, + } + for i, tt := range tests { ++ t.Setenv("GODEBUG", tt.godebug) + gotCookies, gotErr := ParseCookie(tt.line) + if !errors.Is(gotErr, tt.err) { + t.Errorf("#%d ParseCookie got error %v, want error %v", i, gotErr, tt.err) +diff --git a/src/runtime/metrics/doc.go b/src/runtime/metrics/doc.go +index f7ad9c5..079e801 100644 +--- a/src/runtime/metrics/doc.go ++++ b/src/runtime/metrics/doc.go +@@ -272,6 +272,11 @@ Below is the full list of supported metrics, ordered lexicographically. + The number of non-default behaviors executed by the net/http + package due to a non-default GODEBUG=http2server=... setting. + ++ /godebug/non-default-behavior/httpcookiemaxnum:events ++ The number of non-default behaviors executed by the net/http ++ package due to a non-default GODEBUG=httpcookiemaxnum=... ++ setting. ++ + /godebug/non-default-behavior/httplaxcontentlength:events + The number of non-default behaviors executed by the net/http + package due to a non-default GODEBUG=httplaxcontentlength=... +-- +2.43.0 + diff --git a/golang.spec b/golang.spec index 4823d48..ab664e8 100644 --- a/golang.spec +++ b/golang.spec @@ -66,7 +66,7 @@ Name: golang Version: 1.24.2 -Release: 41 +Release: 42 Summary: The Go Programming Language License: BSD and Public Domain URL: https://golang.org/ @@ -142,6 +142,8 @@ Patch1014: 1014-CVE-2025-58185-encoding-asn1-prevent-memory-exhaustion.patch Patch1015: 1015-CVE-2025-58187-crypto-x509-improve-domain-name-verification.patch Patch1016: 1016-CVE-2025-58187-crypto-x509-rework-fix-for-CVE-2025-58187.patch Patch1017: 1017-CVE-2025-61723-encoding-pem-make-Decode-complexity-linear.patch +Patch1018: 1018-CVE-2025-47912-net-url-enforce-stricter-parsing-of.patch +Patch1019: 1019-CVE-2025-58186-net-http-add-httpcookiemaxnum-GODEBUG.patch # Backport of RVA23 Patch2001: 2001-cpu-internal-provide-runtime-detection-of-RISC-V-ext.patch @@ -422,6 +424,12 @@ fi %files devel -f go-tests.list -f go-misc.list -f go-src.list %changelog +* Thu Nov 20 2025 huzhangying - 1.24.2-42 +- Type:CVE +- CVE:CVE-2025-47912,CVE-2025-58186 +- SUG:NA +- DESC:fix CVE-2025-47912,CVE-2025-58186 + * Fri Nov 14 2025 zhaoyifan - 1.24.2-41 - Type:CVE - CVE:CVE-2025-58187,CVE-2025-61723 -- Gitee