package proxy_test import ( "net/http" "net/netip" "reflect" "testing" "time" "sneak.berlin/go/smallwebwaf/internal/bans" "sneak.berlin/go/smallwebwaf/internal/requestlog" ) // trapPaths is the setting's name, and trapPathList what the tests set it // to. const ( trapPaths = "SWWAF_TRAP_PATHS" trapPathList = "/wp-login.php,/xmlrpc.php" ) func TestTrapPathBansAsABanRuleDoes(t *testing.T) { t.Parallel() const allowed = "192.0.2.60" // in SWWAF_ALLOW_NETS // A block rule for the same path: the trap path comes first. s, clk, server := startWithClock(t, "", map[string]string{ trapPaths: trapPathList, rulesDir: writeRules(t, `wp path block ^/wp-login\.php$`), allowNets: allowed, banResponse: "429", }) start := clk.Now() // Only the path itself, as the client sent it, is a trap path. for _, path := range []string{ "/wp-login.php/", "/WP-LOGIN.PHP", "/blog/xmlrpc.php", "/%77p-login.php", } { s.request(otherClient, path, http.StatusOK, requestlog.ActionForward) } // A client in SWWAF_ALLOW_NETS is not checked. s.request(allowed, "/xmlrpc.php", http.StatusOK, requestlog.ActionForward) // The query is not part of the path. line := s.request(client, "/wp-login.php?redirect_to=x", http.StatusTooManyRequests, requestlog.ActionBanned) wantRuleIDs(t, line) if line.BanExpires != requestlog.FormatTime(start.Add(7*24*time.Hour)) { t.Errorf("log line has ban_expires %q, want seven days on", line.BanExpires) } netblock := netip.MustParsePrefix(client + "/32") want := bans.Ban{ Netblock: netblock, Start: start, Expires: start.Add(7 * 24 * time.Hour), Cause: bans.CauseAttack, Reason: "asked for the trap path /wp-login.php", Notes: bans.Notes{ TrapPath: "/wp-login.php", Request: bans.Request{ Time: start, Method: http.MethodGet, Host: appHost, Path: "/wp-login.php?redirect_to=x", Status: http.StatusTooManyRequests, UserAgent: userAgent, }, Requests: 1, }, } got := server.Ledger.Bans(netblock) if len(got) != 1 || !reflect.DeepEqual(got[0], want) { t.Fatalf("bans\n%+v\nwant\n%+v", got, want) } // The next request is refused under the ban, and makes it permanent. line = s.get(client, http.StatusTooManyRequests, requestlog.ActionBanned) if line.BanExpires != permanent { t.Errorf("log line has ban_expires %q, want permanent", line.BanExpires) } } func TestTrapPathsNeedNoRuleFiles(t *testing.T) { t.Parallel() s, _, _ := startWithClock(t, "", map[string]string{ trapPaths: trapPathList, "SWWAF_RULES_ENABLED": "false", }) s.request(client, "/xmlrpc.php", http.StatusForbidden, requestlog.ActionBanned) } func TestObserveModeLogsWhatATrapPathWouldDo(t *testing.T) { t.Parallel() s, _, server := startWithClock(t, "", map[string]string{ trapPaths: trapPathList, mode: observe, }) line := s.request(client, "/xmlrpc.php", http.StatusOK, requestlog.ActionForward) wantWouldAction(t, line, requestlog.ActionBanned) // No ban was made. s.get(client, http.StatusOK, requestlog.ActionForward) if got := server.Ledger.Snapshot(); len(got) != 0 { t.Errorf("bans %+v, want none", got) } }