// This file is part of the happyDomain (R) project. // Copyright (c) 2020-2026 happyDomain // Authors: Pierre-Olivier Mercier, et al. package checker import ( "context" "fmt" "strings" sdk "git.happydns.org/checker-sdk-go/checker" ) func init() { RegisterRule(&cookiePrefixesRule{}) RegisterRule(&cookieSizeRule{}) } // cookiePrefixesRule enforces the cookie name prefix semantics from // RFC 6265bis §4.1.3: // // - Names starting with "__Secure-" MUST have the Secure attribute. // - Names starting with "__Host-" MUST have Secure, MUST NOT have a // Domain attribute, and MUST have Path="/". // // Browsers reject Set-Cookie that violates these constraints, so a // failure here means the cookie is being silently dropped by every // modern user agent. type cookiePrefixesRule struct{} func (r *cookiePrefixesRule) Name() string { return "http.cookie_prefixes" } func (r *cookiePrefixesRule) Description() string { return "Verifies cookies using the __Secure- / __Host- name prefixes meet the RFC 6265bis constraints (Secure, Domain, Path)." } func (r *cookiePrefixesRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState { data, errSt := loadHTTPData(ctx, obs) if errSt != nil { return []sdk.CheckState{*errSt} } probes := successfulHTTPSProbes(data.Probes) if len(probes) == 0 { return []sdk.CheckState{unknownState("http.cookie_prefixes.no_https", "No successful HTTPS probe to evaluate.")} } var states []sdk.CheckState prefixed := 0 for _, p := range probes { for _, c := range p.Cookies { switch { case strings.HasPrefix(c.Name, "__Host-"): prefixed++ issues := hostPrefixIssues(c) if len(issues) > 0 { states = append(states, sdk.CheckState{ Status: sdk.StatusWarn, Code: "http.cookie_prefixes.invalid_host", Subject: fmt.Sprintf("%s :: %s", p.Address, c.Name), Message: fmt.Sprintf("Cookie %q violates the __Host- prefix contract (RFC 6265bis §4.1.3): %s", c.Name, strings.Join(issues, ", ")), }) } case strings.HasPrefix(c.Name, "__Secure-"): prefixed++ if !c.Secure { states = append(states, sdk.CheckState{ Status: sdk.StatusWarn, Code: "http.cookie_prefixes.invalid_secure", Subject: fmt.Sprintf("%s :: %s", p.Address, c.Name), Message: fmt.Sprintf("Cookie %q uses the __Secure- prefix but is not marked Secure (RFC 6265bis §4.1.3); the cookie will be rejected by browsers.", c.Name), }) } } } } if prefixed == 0 { return []sdk.CheckState{{ Status: sdk.StatusInfo, Code: "http.cookie_prefixes.none", Message: "No cookies use the __Host- or __Secure- name prefixes; consider them for high-value cookies (session, CSRF token, …).", }} } if len(states) == 0 { return []sdk.CheckState{passState("http.cookie_prefixes.ok", fmt.Sprintf("All %d prefixed cookies satisfy the RFC 6265bis constraints.", prefixed))} } return states } // hostPrefixIssues returns the list of __Host- contract violations on // the given cookie. Empty slice means the cookie is conformant. func hostPrefixIssues(c CookieInfo) []string { var issues []string if !c.Secure { issues = append(issues, "missing Secure") } if c.Domain != "" { issues = append(issues, "Domain attribute is forbidden") } if c.Path != "/" { issues = append(issues, fmt.Sprintf("Path must be \"/\", got %q", c.Path)) } return issues } // cookieSizeRule flags cookies whose raw Set-Cookie line exceeds the // per-cookie budget (4096 bytes) browsers are required to support per // RFC 6265 §6.1. Anything over is at risk of being silently truncated // or dropped by user agents. type cookieSizeRule struct{} func (r *cookieSizeRule) Name() string { return "http.cookie_size" } func (r *cookieSizeRule) Description() string { return "Flags cookies whose Set-Cookie line exceeds the 4096-byte minimum browsers must support (RFC 6265 §6.1)." } func (r *cookieSizeRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState { data, errSt := loadHTTPData(ctx, obs) if errSt != nil { return []sdk.CheckState{*errSt} } probes := successfulHTTPSProbes(data.Probes) if len(probes) == 0 { return []sdk.CheckState{unknownState("http.cookie_size.no_https", "No successful HTTPS probe to evaluate.")} } var states []sdk.CheckState total := 0 for _, p := range probes { for _, c := range p.Cookies { total++ if c.Size > MaxCookieSize { states = append(states, sdk.CheckState{ Status: sdk.StatusWarn, Code: "http.cookie_size.too_large", Subject: fmt.Sprintf("%s :: %s", p.Address, c.Name), Message: fmt.Sprintf("Cookie %q is %d bytes; RFC 6265 §6.1 only mandates support for cookies up to %d bytes, larger cookies may be silently dropped.", c.Name, c.Size, MaxCookieSize), }) } } } if total == 0 { return []sdk.CheckState{passState("http.cookie_size.none", "No cookies were set on the inspected responses.")} } if len(states) == 0 { return []sdk.CheckState{passState("http.cookie_size.ok", fmt.Sprintf("All %d cookies fit within the %d-byte per-cookie budget.", total, MaxCookieSize))} } return states }