Skip to content

Commit 7aa9ca7

Browse files
committed
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 <[email protected]> Reviewed-by: Mark Freeman <[email protected]> Reviewed-by: Damien Neil <[email protected]>
1 parent f870a1d commit 7aa9ca7

File tree

2 files changed

+90
-1
lines changed

2 files changed

+90
-1
lines changed

src/net/http/cookiejar/jar.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"net"
1313
"net/http"
1414
"net/http/internal/ascii"
15+
"net/netip"
1516
"net/url"
1617
"slices"
1718
"strings"
@@ -120,7 +121,7 @@ func (e *entry) id() string {
120121
// request to host/path. It is the caller's responsibility to check if the
121122
// cookie is expired.
122123
func (e *entry) shouldSend(https bool, host, path string) bool {
123-
return e.domainMatch(host) && e.pathMatch(path) && (https || !e.Secure)
124+
return e.domainMatch(host) && e.pathMatch(path) && e.secureMatch(https)
124125
}
125126

126127
// domainMatch checks whether e's Domain allows sending e back to host.
@@ -148,6 +149,38 @@ func (e *entry) pathMatch(requestPath string) bool {
148149
return false
149150
}
150151

152+
// secureMatch checks whether a cookie should be sent based on the protocol
153+
// and the Secure flag. Localhost is considered a secure origin regardless
154+
// of protocol, matching browser behavior.
155+
func (e *entry) secureMatch(https bool) bool {
156+
if !e.Secure {
157+
// Cookies not marked secure are always sent.
158+
return true
159+
}
160+
// Everything below is about cookies marked secure.
161+
if https {
162+
// HTTPS request matches secure cookies.
163+
return true
164+
}
165+
// Consider localhost to be secure like browsers.
166+
if isLocalhost(e.Domain) {
167+
return true
168+
}
169+
ip, err := netip.ParseAddr(e.Domain)
170+
if err == nil && ip.IsLoopback() {
171+
return true
172+
}
173+
return false
174+
}
175+
176+
func isLocalhost(host string) bool {
177+
host = strings.TrimSuffix(host, ".")
178+
if idx := strings.LastIndex(host, "."); idx >= 0 {
179+
host = host[idx+1:]
180+
}
181+
return ascii.EqualFold(host, "localhost")
182+
}
183+
151184
// hasDotSuffix reports whether s ends in "."+suffix.
152185
func hasDotSuffix(s, suffix string) bool {
153186
return len(s) > len(suffix) && s[len(s)-len(suffix)-1] == '.' && s[len(s)-len(suffix):] == suffix

src/net/http/cookiejar/jar_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,62 @@ var basicsTests = [...]jarTest{
471471
{"https://www.host.test/some/path", "A=a"},
472472
},
473473
},
474+
{
475+
"Secure cookies are sent for localhost",
476+
"http://localhost:8910/",
477+
[]string{"A=a; secure"},
478+
"A=a",
479+
[]query{
480+
{"http://localhost:8910", "A=a"},
481+
{"http://localhost:8910/", "A=a"},
482+
{"http://localhost:8910/some/path", "A=a"},
483+
{"https://localhost:8910", "A=a"},
484+
{"https://localhost:8910/", "A=a"},
485+
{"https://localhost:8910/some/path", "A=a"},
486+
},
487+
},
488+
{
489+
"Secure cookies are sent for localhost (tld)",
490+
"http://example.LOCALHOST:8910/",
491+
[]string{"A=a; secure"},
492+
"A=a",
493+
[]query{
494+
{"http://example.LOCALHOST:8910", "A=a"},
495+
{"http://example.LOCALHOST:8910/", "A=a"},
496+
{"http://example.LOCALHOST:8910/some/path", "A=a"},
497+
{"https://example.LOCALHOST:8910", "A=a"},
498+
{"https://example.LOCALHOST:8910/", "A=a"},
499+
{"https://example.LOCALHOST:8910/some/path", "A=a"},
500+
},
501+
},
502+
{
503+
"Secure cookies are sent for localhost (ipv6)",
504+
"http://[::1]:8910/",
505+
[]string{"A=a; secure"},
506+
"A=a",
507+
[]query{
508+
{"http://[::1]:8910", "A=a"},
509+
{"http://[::1]:8910/", "A=a"},
510+
{"http://[::1]:8910/some/path", "A=a"},
511+
{"https://[::1]:8910", "A=a"},
512+
{"https://[::1]:8910/", "A=a"},
513+
{"https://[::1]:8910/some/path", "A=a"},
514+
},
515+
},
516+
{
517+
"Localhost only if it's a segment",
518+
"http://notlocalhost/",
519+
[]string{"A=a; secure"},
520+
"A=a",
521+
[]query{
522+
{"http://notlocalhost", ""},
523+
{"http://notlocalhost/", ""},
524+
{"http://notlocalhost/some/path", ""},
525+
{"https://notlocalhost", "A=a"},
526+
{"https://notlocalhost/", "A=a"},
527+
{"https://notlocalhost/some/path", "A=a"},
528+
},
529+
},
474530
{
475531
"Explicit path.",
476532
"http://www.host.test/",

0 commit comments

Comments
 (0)