"net"
"net/http"
"net/http/internal/ascii"
+ "net/netip"
"net/url"
"slices"
"strings"
// 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.
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
{"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/",