aboutsummaryrefslogtreecommitdiff
path: root/src/net/http
diff options
context:
space:
mode:
authorSean Liao <sean@liao.dev>2025-11-04 22:47:42 +0000
committerSean Liao <sean@liao.dev>2025-11-21 12:47:29 -0800
commit7aa9ca729fcb063506ea4ed35ee9b2673a853e4f (patch)
treee109da9af42bebdcd112626ff3229ecaf8ed8c42 /src/net/http
parentf870a1d3989d428a5d87368e340f23c4d8232380 (diff)
downloadgo-7aa9ca729fcb063506ea4ed35ee9b2673a853e4f.tar.xz
net/http/cookiejar: treat localhost as secure origin
For development purposes, browsers treat localhost as a secure origin regardless of protocol. Fixes #60997 https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#restrict_access_to_cookies https://bugzilla.mozilla.org/show_bug.cgi?id=1618113 https://issues.chromium.org/issues/40120372 Change-Id: I6d31df4e055f2872c4b93571c53ae5160923852b Reviewed-on: https://go-review.googlesource.com/c/go/+/717860 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Mark Freeman <markfreeman@google.com> Reviewed-by: Damien Neil <dneil@google.com>
Diffstat (limited to 'src/net/http')
-rw-r--r--src/net/http/cookiejar/jar.go35
-rw-r--r--src/net/http/cookiejar/jar_test.go56
2 files changed, 90 insertions, 1 deletions
diff --git a/src/net/http/cookiejar/jar.go b/src/net/http/cookiejar/jar.go
index edf14d03ad..db6bcddb26 100644
--- a/src/net/http/cookiejar/jar.go
+++ b/src/net/http/cookiejar/jar.go
@@ -12,6 +12,7 @@ import (
"net"
"net/http"
"net/http/internal/ascii"
+ "net/netip"
"net/url"
"slices"
"strings"
@@ -120,7 +121,7 @@ func (e *entry) id() string {
// request to host/path. It is the caller's responsibility to check if the
// cookie is expired.
func (e *entry) shouldSend(https bool, host, path string) bool {
- return e.domainMatch(host) && e.pathMatch(path) && (https || !e.Secure)
+ return e.domainMatch(host) && e.pathMatch(path) && e.secureMatch(https)
}
// domainMatch checks whether e's Domain allows sending e back to host.
@@ -148,6 +149,38 @@ func (e *entry) pathMatch(requestPath string) bool {
return false
}
+// secureMatch checks whether a cookie should be sent based on the protocol
+// and the Secure flag. Localhost is considered a secure origin regardless
+// of protocol, matching browser behavior.
+func (e *entry) secureMatch(https bool) bool {
+ if !e.Secure {
+ // Cookies not marked secure are always sent.
+ return true
+ }
+ // Everything below is about cookies marked secure.
+ if https {
+ // HTTPS request matches secure cookies.
+ return true
+ }
+ // Consider localhost to be secure like browsers.
+ if isLocalhost(e.Domain) {
+ return true
+ }
+ ip, err := netip.ParseAddr(e.Domain)
+ if err == nil && ip.IsLoopback() {
+ return true
+ }
+ return false
+}
+
+func isLocalhost(host string) bool {
+ host = strings.TrimSuffix(host, ".")
+ if idx := strings.LastIndex(host, "."); idx >= 0 {
+ host = host[idx+1:]
+ }
+ return ascii.EqualFold(host, "localhost")
+}
+
// hasDotSuffix reports whether s ends in "."+suffix.
func hasDotSuffix(s, suffix string) bool {
return len(s) > len(suffix) && s[len(s)-len(suffix)-1] == '.' && s[len(s)-len(suffix):] == suffix
diff --git a/src/net/http/cookiejar/jar_test.go b/src/net/http/cookiejar/jar_test.go
index 509560170a..feedd6d0e9 100644
--- a/src/net/http/cookiejar/jar_test.go
+++ b/src/net/http/cookiejar/jar_test.go
@@ -472,6 +472,62 @@ var basicsTests = [...]jarTest{
},
},
{
+ "Secure cookies are sent for localhost",
+ "http://localhost:8910/",
+ []string{"A=a; secure"},
+ "A=a",
+ []query{
+ {"http://localhost:8910", "A=a"},
+ {"http://localhost:8910/", "A=a"},
+ {"http://localhost:8910/some/path", "A=a"},
+ {"https://localhost:8910", "A=a"},
+ {"https://localhost:8910/", "A=a"},
+ {"https://localhost:8910/some/path", "A=a"},
+ },
+ },
+ {
+ "Secure cookies are sent for localhost (tld)",
+ "http://example.LOCALHOST:8910/",
+ []string{"A=a; secure"},
+ "A=a",
+ []query{
+ {"http://example.LOCALHOST:8910", "A=a"},
+ {"http://example.LOCALHOST:8910/", "A=a"},
+ {"http://example.LOCALHOST:8910/some/path", "A=a"},
+ {"https://example.LOCALHOST:8910", "A=a"},
+ {"https://example.LOCALHOST:8910/", "A=a"},
+ {"https://example.LOCALHOST:8910/some/path", "A=a"},
+ },
+ },
+ {
+ "Secure cookies are sent for localhost (ipv6)",
+ "http://[::1]:8910/",
+ []string{"A=a; secure"},
+ "A=a",
+ []query{
+ {"http://[::1]:8910", "A=a"},
+ {"http://[::1]:8910/", "A=a"},
+ {"http://[::1]:8910/some/path", "A=a"},
+ {"https://[::1]:8910", "A=a"},
+ {"https://[::1]:8910/", "A=a"},
+ {"https://[::1]:8910/some/path", "A=a"},
+ },
+ },
+ {
+ "Localhost only if it's a segment",
+ "http://notlocalhost/",
+ []string{"A=a; secure"},
+ "A=a",
+ []query{
+ {"http://notlocalhost", ""},
+ {"http://notlocalhost/", ""},
+ {"http://notlocalhost/some/path", ""},
+ {"https://notlocalhost", "A=a"},
+ {"https://notlocalhost/", "A=a"},
+ {"https://notlocalhost/some/path", "A=a"},
+ },
+ },
+ {
"Explicit path.",
"http://www.host.test/",
[]string{"A=a; path=/some/path"},