feat(lib): ensure that clients store cookies (#501)
* feat(lib): ensure that clients store cookies If a client is misconfigured and does not store cookies, then they can get into a proof of work death spiral with Anubis. This fixes the problem by setting a test cookie whenever the user gets hit with a challenge page. If the test cookie is not there at challenge pass time, then they are blocked. Administrators will also get a log message explaining that the user intentionally broke cookie support and that this behavior is not an Anubis bug. Additionally, this ensures that clients being shown a challenge support gzip-compressed responses by showing the challenge page at gzip level 1. This level is intentionally chosen in order to minimize system impacts. The ClearCookie function is made more generic to account for cookie names as an argument. A correlating SetCookie function was also added to make it easier to set cookies. * chore(lib): clean up test code Signed-off-by: Xe Iaso <me@xeiaso.net> --------- Signed-off-by: Xe Iaso <me@xeiaso.net>
This commit is contained in:
parent
9e9982ab5d
commit
b640c567da
11 changed files with 200 additions and 154 deletions
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
|
|
@ -43,10 +44,10 @@ type challenge struct {
|
|||
Challenge string `json:"challenge"`
|
||||
}
|
||||
|
||||
func makeChallenge(t *testing.T, ts *httptest.Server) challenge {
|
||||
func makeChallenge(t *testing.T, ts *httptest.Server, cli *http.Client) challenge {
|
||||
t.Helper()
|
||||
|
||||
resp, err := ts.Client().Post(ts.URL+"/.within.website/x/cmd/anubis/api/make-challenge", "", nil)
|
||||
resp, err := cli.Post(ts.URL+"/.within.website/x/cmd/anubis/api/make-challenge", "", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("can't request challenge: %v", err)
|
||||
}
|
||||
|
|
@ -60,6 +61,54 @@ func makeChallenge(t *testing.T, ts *httptest.Server) challenge {
|
|||
return chall
|
||||
}
|
||||
|
||||
func handleChallengeZeroDifficulty(t *testing.T, ts *httptest.Server, cli *http.Client, chall challenge) *http.Response {
|
||||
t.Helper()
|
||||
|
||||
nonce := 0
|
||||
elapsedTime := 420
|
||||
redir := "/"
|
||||
calculated := ""
|
||||
calcString := fmt.Sprintf("%s%d", chall.Challenge, nonce)
|
||||
calculated = internal.SHA256sum(calcString)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, ts.URL+"/.within.website/x/cmd/anubis/api/pass-challenge", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("can't make request: %v", err)
|
||||
}
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Set("response", calculated)
|
||||
q.Set("nonce", fmt.Sprint(nonce))
|
||||
q.Set("redir", redir)
|
||||
q.Set("elapsedTime", fmt.Sprint(elapsedTime))
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
resp, err := cli.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("can't do request: %v", err)
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func httpClient(t *testing.T) *http.Client {
|
||||
t.Helper()
|
||||
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cli := &http.Client{
|
||||
Jar: jar,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
return cli
|
||||
}
|
||||
|
||||
func TestLoadPolicies(t *testing.T) {
|
||||
for _, fname := range []string{"botPolicies.json", "botPolicies.yaml"} {
|
||||
t.Run(fname, func(t *testing.T) {
|
||||
|
|
@ -85,7 +134,6 @@ func TestCVE2025_24369(t *testing.T) {
|
|||
Next: http.NewServeMux(),
|
||||
Policy: pol,
|
||||
|
||||
CookieDomain: ".local.cetacean.club",
|
||||
CookiePartitioned: true,
|
||||
CookieName: t.Name(),
|
||||
})
|
||||
|
|
@ -93,34 +141,9 @@ func TestCVE2025_24369(t *testing.T) {
|
|||
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
|
||||
defer ts.Close()
|
||||
|
||||
chall := makeChallenge(t, ts)
|
||||
calcString := fmt.Sprintf("%s%d", chall.Challenge, 0)
|
||||
calculated := internal.SHA256sum(calcString)
|
||||
nonce := 0
|
||||
elapsedTime := 420
|
||||
redir := "/"
|
||||
|
||||
cli := ts.Client()
|
||||
cli.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, ts.URL+"/.within.website/x/cmd/anubis/api/pass-challenge", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("can't make request: %v", err)
|
||||
}
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Set("response", calculated)
|
||||
q.Set("nonce", fmt.Sprint(nonce))
|
||||
q.Set("redir", redir)
|
||||
q.Set("elapsedTime", fmt.Sprint(elapsedTime))
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
resp, err := cli.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("can't do challenge passing")
|
||||
}
|
||||
cli := httpClient(t)
|
||||
chall := makeChallenge(t, ts, cli)
|
||||
resp := handleChallengeZeroDifficulty(t, ts, cli, chall)
|
||||
|
||||
if resp.StatusCode == http.StatusFound {
|
||||
t.Log("Regression on CVE-2025-24369")
|
||||
|
|
@ -137,58 +160,18 @@ func TestCookieCustomExpiration(t *testing.T) {
|
|||
Next: http.NewServeMux(),
|
||||
Policy: pol,
|
||||
|
||||
CookieDomain: "local.cetacean.club",
|
||||
CookieName: t.Name(),
|
||||
CookieExpiration: ckieExpiration,
|
||||
})
|
||||
|
||||
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
|
||||
defer ts.Close()
|
||||
|
||||
cli := &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
cli := httpClient(t)
|
||||
chall := makeChallenge(t, ts, cli)
|
||||
|
||||
resp, err := cli.Post(ts.URL+"/.within.website/x/cmd/anubis/api/make-challenge", "", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("can't request challenge: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var chall = struct {
|
||||
Challenge string `json:"challenge"`
|
||||
}{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&chall); err != nil {
|
||||
t.Fatalf("can't read challenge response body: %v", err)
|
||||
}
|
||||
|
||||
nonce := 0
|
||||
elapsedTime := 420
|
||||
redir := "/"
|
||||
calculated := ""
|
||||
calcString := fmt.Sprintf("%s%d", chall.Challenge, nonce)
|
||||
calculated = internal.SHA256sum(calcString)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, ts.URL+"/.within.website/x/cmd/anubis/api/pass-challenge", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("can't make request: %v", err)
|
||||
}
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Set("response", calculated)
|
||||
q.Set("nonce", fmt.Sprint(nonce))
|
||||
q.Set("redir", redir)
|
||||
q.Set("elapsedTime", fmt.Sprint(elapsedTime))
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
requestReceiveLowerBound := time.Now()
|
||||
resp, err = cli.Do(req)
|
||||
requestReceiveLowerBound := time.Now().Add(-1 * time.Minute)
|
||||
resp := handleChallengeZeroDifficulty(t, ts, cli, chall)
|
||||
requestReceiveUpperBound := time.Now()
|
||||
if err != nil {
|
||||
t.Fatalf("can't do challenge passing")
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusFound {
|
||||
resp.Write(os.Stderr)
|
||||
|
|
@ -226,59 +209,21 @@ func TestCookieSettings(t *testing.T) {
|
|||
Next: http.NewServeMux(),
|
||||
Policy: pol,
|
||||
|
||||
CookieDomain: "local.cetacean.club",
|
||||
CookieDomain: "127.0.0.1",
|
||||
CookiePartitioned: true,
|
||||
CookieName: t.Name(),
|
||||
CookieExpiration: anubis.CookieDefaultExpirationTime,
|
||||
})
|
||||
|
||||
requestReceiveLowerBound := time.Now()
|
||||
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
|
||||
defer ts.Close()
|
||||
|
||||
cli := &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
cli := httpClient(t)
|
||||
chall := makeChallenge(t, ts, cli)
|
||||
|
||||
resp, err := cli.Post(ts.URL+"/.within.website/x/cmd/anubis/api/make-challenge", "", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("can't request challenge: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var chall = struct {
|
||||
Challenge string `json:"challenge"`
|
||||
}{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&chall); err != nil {
|
||||
t.Fatalf("can't read challenge response body: %v", err)
|
||||
}
|
||||
|
||||
nonce := 0
|
||||
elapsedTime := 420
|
||||
redir := "/"
|
||||
calculated := ""
|
||||
calcString := fmt.Sprintf("%s%d", chall.Challenge, nonce)
|
||||
calculated = internal.SHA256sum(calcString)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, ts.URL+"/.within.website/x/cmd/anubis/api/pass-challenge", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("can't make request: %v", err)
|
||||
}
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Set("response", calculated)
|
||||
q.Set("nonce", fmt.Sprint(nonce))
|
||||
q.Set("redir", redir)
|
||||
q.Set("elapsedTime", fmt.Sprint(elapsedTime))
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
requestReceiveLowerBound := time.Now()
|
||||
resp, err = cli.Do(req)
|
||||
resp := handleChallengeZeroDifficulty(t, ts, cli, chall)
|
||||
requestReceiveUpperBound := time.Now()
|
||||
if err != nil {
|
||||
t.Fatalf("can't do challenge passing")
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusFound {
|
||||
resp.Write(os.Stderr)
|
||||
|
|
@ -298,8 +243,8 @@ func TestCookieSettings(t *testing.T) {
|
|||
return
|
||||
}
|
||||
|
||||
if ckie.Domain != "local.cetacean.club" {
|
||||
t.Errorf("cookie domain is wrong, wanted local.cetacean.club, got: %s", ckie.Domain)
|
||||
if ckie.Domain != "127.0.0.1" {
|
||||
t.Errorf("cookie domain is wrong, wanted 127.0.0.1, got: %s", ckie.Domain)
|
||||
}
|
||||
|
||||
expirationLowerBound := requestReceiveLowerBound.Add(anubis.CookieDefaultExpirationTime)
|
||||
|
|
@ -457,6 +402,10 @@ func TestBasePrefix(t *testing.T) {
|
|||
t.Fatalf("can't make request: %v", err)
|
||||
}
|
||||
|
||||
for _, ckie := range resp.Cookies() {
|
||||
req.AddCookie(ckie)
|
||||
}
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Set("response", calculated)
|
||||
q.Set("nonce", fmt.Sprint(nonce))
|
||||
|
|
@ -561,6 +510,25 @@ func TestCloudflareWorkersRule(t *testing.T) {
|
|||
t.Fatalf("can't construct libanubis.Server: %v", err)
|
||||
}
|
||||
|
||||
t.Run("with-cf-worker-header", func(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet, "/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req.Header.Add("X-Real-Ip", "127.0.0.1")
|
||||
req.Header.Add("Cf-Worker", "true")
|
||||
|
||||
cr, _, err := s.check(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if cr.Rule != config.RuleDeny {
|
||||
t.Errorf("rule is wrong, wanted %s, got: %s", config.RuleDeny, cr.Rule)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no-cf-worker-header", func(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet, "/", nil)
|
||||
if err != nil {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue