fix: Dynamic cookie domain not working (#731)

* Fix cookieDynamicDomain option not being set in Options struct

* Fix using wrong cookie name when using dynamic cookie domains

* Adjust testcases for new cookie option structs

* Add known words to expect.txt and change typo in Zombocom

* Cleanup expect.txt

* Add changes to changelog

* Bump versions of grpc and apimachinery

* Fix testcases and add additional condition for dynamic cookie domain
This commit is contained in:
Martin 2025-06-29 21:38:55 +02:00 committed by GitHub
parent b1edf84a7c
commit 6aa17532da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 98 additions and 84 deletions

View file

@ -69,7 +69,6 @@ type Server struct {
policy *policy.ParsedConfig
DNSBLCache *decaymap.Impl[string, dnsbl.DroneBLResponse]
OGTags *ogtags.OGTagCache
cookieName string
ed25519Priv ed25519.PrivateKey
hs512Secret []byte
opts Options
@ -88,8 +87,6 @@ func (s *Server) getTokenKeyfunc() jwt.Keyfunc {
}
}
func (s *Server) challengeFor(r *http.Request, difficulty int) string {
var fp [32]byte
if len(s.hs512Secret) == 0 {
@ -149,24 +146,24 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS
return
}
ckie, err := r.Cookie(s.cookieName)
ckie, err := r.Cookie(anubis.CookieName)
if err != nil {
lg.Debug("cookie not found", "path", r.URL.Path)
s.ClearCookie(w, s.cookieName, cookiePath, r.Host)
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
s.RenderIndex(w, r, rule, httpStatusOnly)
return
}
if err := ckie.Valid(); err != nil {
lg.Debug("cookie is invalid", "err", err)
s.ClearCookie(w, s.cookieName, cookiePath, r.Host)
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
s.RenderIndex(w, r, rule, httpStatusOnly)
return
}
if time.Now().After(ckie.Expires) && !ckie.Expires.IsZero() {
lg.Debug("cookie expired", "path", r.URL.Path)
s.ClearCookie(w, s.cookieName, cookiePath, r.Host)
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
s.RenderIndex(w, r, rule, httpStatusOnly)
return
}
@ -175,7 +172,7 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS
if err != nil || !token.Valid {
lg.Debug("invalid token", "path", r.URL.Path, "err", err)
s.ClearCookie(w, s.cookieName, cookiePath, r.Host)
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
s.RenderIndex(w, r, rule, httpStatusOnly)
return
}
@ -183,7 +180,7 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
lg.Debug("invalid token claims type", "path", r.URL.Path)
s.ClearCookie(w, s.cookieName, cookiePath, r.Host)
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
s.RenderIndex(w, r, rule, httpStatusOnly)
return
}
@ -191,14 +188,14 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS
policyRule, ok := claims["policyRule"].(string)
if !ok {
lg.Debug("policyRule claim is not a string")
s.ClearCookie(w, s.cookieName, cookiePath, r.Host)
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
s.RenderIndex(w, r, rule, httpStatusOnly)
return
}
if policyRule != rule.Hash() {
lg.Debug("user originally passed with a different rule, issuing new challenge", "old", policyRule, "new", rule.Name)
s.ClearCookie(w, s.cookieName, cookiePath, r.Host)
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
s.RenderIndex(w, r, rule, httpStatusOnly)
return
}
@ -222,7 +219,7 @@ func (s *Server) checkRules(w http.ResponseWriter, r *http.Request, cr policy.Ch
s.ServeHTTPNext(w, r)
return true
case config.RuleDeny:
s.ClearCookie(w, s.cookieName, cookiePath, r.Host)
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
lg.Info("explicit deny")
if rule == nil {
lg.Error("rule is nil, cannot calculate checksum")
@ -241,7 +238,7 @@ func (s *Server) checkRules(w http.ResponseWriter, r *http.Request, cr policy.Ch
s.RenderBench(w, r)
return true
default:
s.ClearCookie(w, s.cookieName, cookiePath, r.Host)
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
slog.Error("CONFIG ERROR: unknown rule", "rule", cr.Rule)
s.respondWithError(w, r, fmt.Sprintf("%s \"maybeReverseProxy.Rules\"", localizer.T("internal_server_error")))
return true
@ -265,10 +262,10 @@ func (s *Server) handleDNSBL(w http.ResponseWriter, r *http.Request, ip string,
if resp != dnsbl.AllGood {
lg.Info("DNSBL hit", "status", resp.String())
localizer := localization.GetLocalizer(r)
s.respondWithStatus(w, r, fmt.Sprintf("%s: %s, %s https://dronebl.org/lookup?ip=%s",
localizer.T("dronebl_entry"),
resp.String(),
localizer.T("see_dronebl_lookup"),
s.respondWithStatus(w, r, fmt.Sprintf("%s: %s, %s https://dronebl.org/lookup?ip=%s",
localizer.T("dronebl_entry"),
resp.String(),
localizer.T("see_dronebl_lookup"),
ip), s.policy.StatusCodes.Deny)
return true
}
@ -314,7 +311,7 @@ func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
lg = lg.With("check_result", cr)
chal := s.challengeFor(r, rule.Challenge.Difficulty)
s.SetCookie(w, anubis.TestCookieName, chal, "/", r.Host)
s.SetCookie(w, CookieOpts{Host: r.Host, Name: anubis.TestCookieName, Value: chal})
err = encoder.Encode(struct {
Rules *config.ChallengeRules `json:"rules"`
@ -343,14 +340,14 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
}
if _, err := r.Cookie(anubis.TestCookieName); errors.Is(err, http.ErrNoCookie) {
s.ClearCookie(w, s.cookieName, cookiePath, r.Host)
s.ClearCookie(w, anubis.TestCookieName, "/", r.Host)
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
s.ClearCookie(w, CookieOpts{Name: anubis.TestCookieName, Host: r.Host})
lg.Warn("user has cookies disabled, this is not an anubis bug")
s.respondWithError(w, r, localizer.T("cookies_disabled"))
return
}
s.ClearCookie(w, anubis.TestCookieName, "/", r.Host)
s.ClearCookie(w, CookieOpts{Name: anubis.TestCookieName, Host: r.Host})
redir := r.FormValue("redir")
redirURL, err := url.ParseRequestURI(redir)
@ -392,7 +389,7 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
if err := impl.Validate(r, lg, rule, challengeStr); err != nil {
failedValidations.WithLabelValues(rule.Challenge.Algorithm).Inc()
var cerr *challenge.Error
s.ClearCookie(w, s.cookieName, cookiePath, r.Host)
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
lg.Debug("challenge validate call failed", "err", err)
switch {
@ -415,12 +412,12 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
})
if err != nil {
lg.Error("failed to sign JWT", "err", err)
s.ClearCookie(w, s.cookieName, cookiePath, r.Host)
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
s.respondWithError(w, r, localizer.T("failed_to_sign_jwt"))
return
}
s.SetCookie(w, s.cookieName, tokenString, cookiePath, r.Host)
s.SetCookie(w, CookieOpts{Path: cookiePath, Host: r.Host, Value: tokenString})
challengesValidated.WithLabelValues(rule.Challenge.Algorithm).Inc()
lg.Debug("challenge passed, redirecting to app")

View file

@ -189,8 +189,6 @@ func TestCVE2025_24369(t *testing.T) {
srv := spawnAnubis(t, Options{
Next: http.NewServeMux(),
Policy: pol,
CookieName: t.Name(),
})
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
@ -235,13 +233,13 @@ func TestCookieCustomExpiration(t *testing.T) {
var ckie *http.Cookie
for _, cookie := range resp.Cookies() {
t.Logf("%#v", cookie)
if cookie.Name == srv.cookieName {
if cookie.Name == anubis.CookieName {
ckie = cookie
break
}
}
if ckie == nil {
t.Errorf("Cookie %q not found", srv.cookieName)
t.Errorf("Cookie %q not found", anubis.CookieName)
return
}
@ -264,7 +262,6 @@ func TestCookieSettings(t *testing.T) {
CookieDomain: "127.0.0.1",
CookiePartitioned: true,
CookieName: t.Name(),
CookieExpiration: anubis.CookieDefaultExpirationTime,
})
@ -286,13 +283,13 @@ func TestCookieSettings(t *testing.T) {
var ckie *http.Cookie
for _, cookie := range resp.Cookies() {
t.Logf("%#v", cookie)
if cookie.Name == srv.cookieName {
if cookie.Name == anubis.CookieName {
ckie = cookie
break
}
}
if ckie == nil {
t.Errorf("Cookie %q not found", srv.cookieName)
t.Errorf("Cookie %q not found", anubis.CookieName)
return
}
@ -619,7 +616,6 @@ func TestRuleChange(t *testing.T) {
Policy: pol,
CookieDomain: "127.0.0.1",
CookieName: t.Name(),
CookieExpiration: ckieExpiration,
})

View file

@ -35,7 +35,6 @@ type Options struct {
CookieDynamicDomain bool
CookieDomain string
CookieExpiration time.Duration
CookieName string
CookiePartitioned bool
BasePrefix string
WebmasterEmail string
@ -102,12 +101,6 @@ func New(opts Options) (*Server, error) {
anubis.BasePrefix = opts.BasePrefix
cookieName := anubis.CookieName
if opts.CookieDomain != "" {
cookieName = anubis.WithDomainCookieName + opts.CookieDomain
}
result := &Server{
next: opts.Next,
ed25519Priv: opts.ED25519PrivateKey,
@ -116,7 +109,6 @@ func New(opts Options) (*Server, error) {
opts: opts,
DNSBLCache: decaymap.New[string, dnsbl.DroneBLResponse](),
OGTags: ogtags.NewOGTagCache(opts.Target, opts.Policy.OpenGraph),
cookieName: cookieName,
}
mux := http.NewServeMux()

View file

@ -22,18 +22,32 @@ import (
var domainMatchRegexp = regexp.MustCompile(`^((xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$`)
func (s *Server) SetCookie(w http.ResponseWriter, name, value, path, host string) {
type CookieOpts struct {
Value string
Host string
Path string
Name string
}
func (s *Server) SetCookie(w http.ResponseWriter, cookieOpts CookieOpts) {
var domain = s.opts.CookieDomain
if s.opts.CookieDynamicDomain && domainMatchRegexp.MatchString(host) {
if etld, err := publicsuffix.EffectiveTLDPlusOne(host); err == nil {
var name = anubis.CookieName
var path = "/"
if cookieOpts.Name != "" {
name = cookieOpts.Name
}
if cookieOpts.Path != "" {
path = cookieOpts.Path
}
if s.opts.CookieDynamicDomain && domainMatchRegexp.MatchString(cookieOpts.Host) {
if etld, err := publicsuffix.EffectiveTLDPlusOne(cookieOpts.Host); err == nil {
domain = etld
name = anubis.WithDomainCookieName + etld
}
}
http.SetCookie(w, &http.Cookie{
Name: name,
Value: value,
Value: cookieOpts.Value,
Expires: time.Now().Add(s.opts.CookieExpiration),
SameSite: http.SameSiteLaxMode,
Domain: domain,
@ -42,12 +56,19 @@ func (s *Server) SetCookie(w http.ResponseWriter, name, value, path, host string
})
}
func (s *Server) ClearCookie(w http.ResponseWriter, name, path, host string) {
func (s *Server) ClearCookie(w http.ResponseWriter, cookieOpts CookieOpts) {
var domain = s.opts.CookieDomain
if s.opts.CookieDynamicDomain && domainMatchRegexp.MatchString(host) {
if etld, err := publicsuffix.EffectiveTLDPlusOne(host); err == nil {
var name = anubis.CookieName
var path = "/"
if cookieOpts.Name != "" {
name = cookieOpts.Name
}
if cookieOpts.Path != "" {
path = cookieOpts.Path
}
if s.opts.CookieDynamicDomain && domainMatchRegexp.MatchString(cookieOpts.Host) {
if etld, err := publicsuffix.EffectiveTLDPlusOne(cookieOpts.Host); err == nil {
domain = etld
name = anubis.WithDomainCookieName + etld
}
}

View file

@ -24,20 +24,20 @@ func TestSetCookie(t *testing.T) {
name: "domain techaro.lol",
options: Options{CookieDomain: "techaro.lol"},
host: "",
cookieName: anubis.WithDomainCookieName + "techaro.lol",
cookieName: anubis.CookieName,
},
{
name: "dynamic cookie domain",
options: Options{CookieDynamicDomain: true},
host: "techaro.lol",
cookieName: anubis.WithDomainCookieName + "techaro.lol",
cookieName: anubis.CookieName,
},
} {
t.Run(tt.name, func(t *testing.T) {
srv := spawnAnubis(t, tt.options)
rw := httptest.NewRecorder()
srv.SetCookie(rw, srv.cookieName, "test", "/", tt.host)
srv.SetCookie(rw, CookieOpts{Value: "test", Host: tt.host})
resp := rw.Result()
cookies := resp.Cookies()
@ -55,7 +55,7 @@ func TestClearCookie(t *testing.T) {
srv := spawnAnubis(t, Options{})
rw := httptest.NewRecorder()
srv.ClearCookie(rw, srv.cookieName, "/", "localhost")
srv.ClearCookie(rw, CookieOpts{Host: "localhost"})
resp := rw.Result()
@ -80,7 +80,7 @@ func TestClearCookieWithDomain(t *testing.T) {
srv := spawnAnubis(t, Options{CookieDomain: "techaro.lol"})
rw := httptest.NewRecorder()
srv.ClearCookie(rw, srv.cookieName, "/", "locahost")
srv.ClearCookie(rw, CookieOpts{Host: "localhost"})
resp := rw.Result()
@ -92,8 +92,8 @@ func TestClearCookieWithDomain(t *testing.T) {
ckie := cookies[0]
if ckie.Name != srv.cookieName {
t.Errorf("wanted cookie named %q, got cookie named %q", srv.cookieName, ckie.Name)
if ckie.Name != anubis.CookieName {
t.Errorf("wanted cookie named %q, got cookie named %q", anubis.CookieName, ckie.Name)
}
if ckie.MaxAge != -1 {
@ -105,7 +105,7 @@ func TestClearCookieWithDynamicDomain(t *testing.T) {
srv := spawnAnubis(t, Options{CookieDynamicDomain: true})
rw := httptest.NewRecorder()
srv.ClearCookie(rw, srv.cookieName, "/", "xeiaso.net")
srv.ClearCookie(rw, CookieOpts{Host: "subdomain.xeiaso.net"})
resp := rw.Result()
@ -117,8 +117,12 @@ func TestClearCookieWithDynamicDomain(t *testing.T) {
ckie := cookies[0]
if ckie.Name != anubis.WithDomainCookieName+"xeiaso.net" {
t.Errorf("wanted cookie named %q, got cookie named %q", srv.cookieName, ckie.Name)
if ckie.Name != anubis.CookieName {
t.Errorf("wanted cookie named %q, got cookie named %q", anubis.CookieName, ckie.Name)
}
if ckie.Domain != "xeiaso.net" {
t.Errorf("wanted cookie domain %q, got cookie domain %q", "xeiaso.net", ckie.Domain)
}
if ckie.MaxAge != -1 {