]> Cypherpunks repositories - gostls13.git/commitdiff
net/http/cookiejar: treat localhost as secure origin
authorSean Liao <sean@liao.dev>
Tue, 4 Nov 2025 22:47:42 +0000 (22:47 +0000)
committerSean Liao <sean@liao.dev>
Fri, 21 Nov 2025 20:47:29 +0000 (12:47 -0800)
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>
src/net/http/cookiejar/jar.go
src/net/http/cookiejar/jar_test.go

index edf14d03ad38091f8a40cbe1d0abd17e0bc1765d..db6bcddb268045beba37fd717abb4635a477dcc5 100644 (file)
@@ -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
index 509560170a592fb9362d17512e4d4f8245691828..feedd6d0e9485ff6e20a4d7309028edaa0d1760f 100644 (file)
@@ -471,6 +471,62 @@ var basicsTests = [...]jarTest{
                        {"https://www.host.test/some/path", "A=a"},
                },
        },
+       {
+               "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/",