mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-06-09 13:58:14 +00:00
926f3d1d0e
This is based on private evaluation of a prerelease security product. I cannot comment further other than I am impressed by its output. This commit is a squash of several commits. The impactful commits have details underneath markdown heading twos. ## fix(metrics): don't expose pprof by default pprof[1] is the Go standard library profiling toolkit. It is invaluable for diagnosing how Go programs perform in the wild. However it also is able to expose secret data set with command line flags. This is not ideal and should be mitigated by correctly configured firewall rules. We don't live in a world where people correctly configure firewall rules, so we have to fix things for people. Welcome to 2026. [1]: https://pkg.go.dev/runtime/pprof Ref: AWOO-001 ## fix(honeypot/naive): cap r9k delay to one second Otherwise this can get unbounded, which can cause problems with lesser HTTP proxies such as Apache. Ref: AWOO-002 ## fix(policy): mend an edge case with subrequest auth and query strings This fixes an unlikely edge case where using subrequest auth and query strings with path based filtering can cause reality to differ from administrator intent. This effectively strips the query string from subrequest auth checks. This deficiency should be fixed in the future. Ref: AWOO-004 ## fix(expressions): mend possible nil pointer deref edge case If Anubis just started up, load averages may not be set in memory. This can cause a nil pointer dereference which could fail requests with weird errors until the async thread sets the load averages. Ref: AWOO-005 ## fix(lib): mend case where domainless redirects could allow cross-domain redirects Ref: AWOO-009 ## fix(expressions): validate randInt bounds before rand.IntN Non-positive or platform-overflowing arguments to the CEL randInt helper used to reach rand.IntN unchecked, surfacing a CEL evaluator error during request processing when policies passed attacker-influenced values (e.g. contentLength). Reject non-positive bounds and detect int narrowing explicitly, returning a typed CEL error in both cases. Ref: AWOO-010 Signed-off-by: Xe Iaso <xe.iaso@techaro.lol>
240 lines
6.0 KiB
Go
240 lines
6.0 KiB
Go
package lib
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"testing"
|
|
|
|
"github.com/TecharoHQ/anubis"
|
|
"github.com/TecharoHQ/anubis/internal"
|
|
"github.com/TecharoHQ/anubis/lib/policy"
|
|
)
|
|
|
|
func TestSetCookie(t *testing.T) {
|
|
for _, tt := range []struct {
|
|
name string
|
|
host string
|
|
cookieName string
|
|
options Options
|
|
}{
|
|
{
|
|
name: "basic",
|
|
options: Options{},
|
|
host: "",
|
|
cookieName: anubis.CookieName,
|
|
},
|
|
{
|
|
name: "domain techaro.lol",
|
|
options: Options{CookieDomain: "techaro.lol"},
|
|
host: "",
|
|
cookieName: anubis.CookieName,
|
|
},
|
|
{
|
|
name: "dynamic cookie domain",
|
|
options: Options{CookieDynamicDomain: true},
|
|
host: "techaro.lol",
|
|
cookieName: anubis.CookieName,
|
|
},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
srv := spawnAnubis(t, tt.options)
|
|
rw := httptest.NewRecorder()
|
|
|
|
srv.SetCookie(rw, CookieOpts{Value: "test", Host: tt.host})
|
|
|
|
resp := rw.Result()
|
|
cookies := resp.Cookies()
|
|
|
|
ckie := cookies[0]
|
|
|
|
if ckie.Name != tt.cookieName {
|
|
t.Errorf("wanted cookie named %q, got cookie named %q", tt.cookieName, ckie.Name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClearCookie(t *testing.T) {
|
|
srv := spawnAnubis(t, Options{})
|
|
rw := httptest.NewRecorder()
|
|
|
|
srv.ClearCookie(rw, CookieOpts{Host: "localhost"})
|
|
|
|
resp := rw.Result()
|
|
|
|
cookies := resp.Cookies()
|
|
|
|
if len(cookies) != 1 {
|
|
t.Errorf("wanted 1 cookie, got %d cookies", len(cookies))
|
|
}
|
|
|
|
ckie := cookies[0]
|
|
|
|
if ckie.Name != anubis.CookieName {
|
|
t.Errorf("wanted cookie named %q, got cookie named %q", anubis.CookieName, ckie.Name)
|
|
}
|
|
|
|
if ckie.MaxAge != -1 {
|
|
t.Errorf("wanted cookie max age of -1, got: %d", ckie.MaxAge)
|
|
}
|
|
}
|
|
|
|
func TestClearCookieWithDomain(t *testing.T) {
|
|
srv := spawnAnubis(t, Options{CookieDomain: "techaro.lol"})
|
|
rw := httptest.NewRecorder()
|
|
|
|
srv.ClearCookie(rw, CookieOpts{Host: "localhost"})
|
|
|
|
resp := rw.Result()
|
|
|
|
cookies := resp.Cookies()
|
|
|
|
if len(cookies) != 1 {
|
|
t.Errorf("wanted 1 cookie, got %d cookies", len(cookies))
|
|
}
|
|
|
|
ckie := cookies[0]
|
|
|
|
if ckie.Name != anubis.CookieName {
|
|
t.Errorf("wanted cookie named %q, got cookie named %q", anubis.CookieName, ckie.Name)
|
|
}
|
|
|
|
if ckie.MaxAge != -1 {
|
|
t.Errorf("wanted cookie max age of -1, got: %d", ckie.MaxAge)
|
|
}
|
|
}
|
|
|
|
func TestClearCookieWithDynamicDomain(t *testing.T) {
|
|
srv := spawnAnubis(t, Options{CookieDynamicDomain: true})
|
|
rw := httptest.NewRecorder()
|
|
|
|
srv.ClearCookie(rw, CookieOpts{Host: "subdomain.xeiaso.net"})
|
|
|
|
resp := rw.Result()
|
|
|
|
cookies := resp.Cookies()
|
|
|
|
if len(cookies) != 1 {
|
|
t.Errorf("wanted 1 cookie, got %d cookies", len(cookies))
|
|
}
|
|
|
|
ckie := cookies[0]
|
|
|
|
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 {
|
|
t.Errorf("wanted cookie max age of -1, got: %d", ckie.MaxAge)
|
|
}
|
|
}
|
|
|
|
func TestRenderIndexRedirect(t *testing.T) {
|
|
s := &Server{
|
|
opts: Options{
|
|
PublicUrl: "https://anubis.example.com",
|
|
},
|
|
}
|
|
req := httptest.NewRequest("GET", "/", nil)
|
|
req.Header.Set("X-Forwarded-Proto", "https")
|
|
req.Header.Set("X-Forwarded-Host", "example.com")
|
|
req.Header.Set("X-Forwarded-Uri", "/foo")
|
|
|
|
rr := httptest.NewRecorder()
|
|
s.RenderIndex(rr, req, policy.CheckResult{}, nil, true)
|
|
|
|
if rr.Code != http.StatusTemporaryRedirect {
|
|
t.Errorf("expected status %d, got %d", http.StatusTemporaryRedirect, rr.Code)
|
|
}
|
|
location := rr.Header().Get("Location")
|
|
parsedURL, err := url.Parse(location)
|
|
if err != nil {
|
|
t.Fatalf("failed to parse location URL %q: %v", location, err)
|
|
}
|
|
|
|
scheme := "https"
|
|
if parsedURL.Scheme != scheme {
|
|
t.Errorf("expected scheme to be %q, got %q", scheme, parsedURL.Scheme)
|
|
}
|
|
|
|
host := "anubis.example.com"
|
|
if parsedURL.Host != host {
|
|
t.Errorf("expected url to be %q, got %q", host, parsedURL.Host)
|
|
}
|
|
|
|
redir := parsedURL.Query().Get("redir")
|
|
expectedRedir := "https://example.com/foo"
|
|
if redir != expectedRedir {
|
|
t.Errorf("expected redir param to be %q, got %q", expectedRedir, redir)
|
|
}
|
|
}
|
|
|
|
func TestRenderIndexUnauthorized(t *testing.T) {
|
|
s := &Server{
|
|
opts: Options{
|
|
PublicUrl: "",
|
|
},
|
|
}
|
|
req := httptest.NewRequest("GET", "/", nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
s.RenderIndex(rr, req, policy.CheckResult{}, nil, true)
|
|
|
|
if rr.Code != http.StatusUnauthorized {
|
|
t.Errorf("expected status %d, got %d", http.StatusUnauthorized, rr.Code)
|
|
}
|
|
if body := rr.Body.String(); body != "Authorization required" {
|
|
t.Errorf("expected body %q, got %q", "Authorization required", body)
|
|
}
|
|
}
|
|
|
|
func TestNoCacheOnError(t *testing.T) {
|
|
pol := loadPolicies(t, "testdata/useragent.yaml", 0)
|
|
srv := spawnAnubis(t, Options{Policy: pol})
|
|
ts := httptest.NewServer(internal.RemoteXRealIP(true, "tcp", srv))
|
|
defer ts.Close()
|
|
|
|
for userAgent, expectedCacheControl := range map[string]string{
|
|
"DENY": "no-store",
|
|
"CHALLENGE": "no-store",
|
|
"ALLOW": "",
|
|
} {
|
|
t.Run(userAgent, func(t *testing.T) {
|
|
req, err := http.NewRequest(http.MethodGet, ts.URL, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
req.Header.Set("User-Agent", userAgent)
|
|
|
|
resp, err := ts.Client().Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if resp.Header.Get("Cache-Control") != expectedCacheControl {
|
|
t.Errorf("wanted Cache-Control header %q, got %q", expectedCacheControl, resp.Header.Get("Cache-Control"))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRejectsHostlessRedirect(t *testing.T) {
|
|
pol := loadPolicies(t, "testdata/useragent.yaml", 0)
|
|
srv := spawnAnubis(t, Options{Policy: pol, RedirectDomains: []string{"allowed.example"}})
|
|
req := httptest.NewRequest(http.MethodGet, "https://anubis.example/.within.website/?redir=%2f%2fevil.example%2fphish", nil)
|
|
rr := httptest.NewRecorder()
|
|
srv.ServeHTTPNext(rr, req)
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected hostless redirect to be rejected, got HTTP %d body %q", rr.Code, rr.Body.String())
|
|
}
|
|
if got := rr.Header().Get("Location"); got != "" {
|
|
t.Fatalf("expected no Location header on rejected redirect, got %q", got)
|
|
}
|
|
}
|