package coreruleset_test import ( "net/http" "slices" "testing" "sneak.berlin/go/smallwebwaf/internal/coreruleset" ) // forgejo is Forgejo's name in SWW_APPS. const forgejo = "forgejo" // forgejoHost is the hostname of the tests of SWW_APPS that serves Forgejo. const forgejoHost = "code.example" func TestForgejoFixesApplyOnlyToAHostnameThatServesForgejo(t *testing.T) { t.Parallel() crs := servingApps(t, map[string][]string{ forgejoHost: {forgejo}, otherHost: {wordpress}, }) // A search of a wiki, a search of the admin page, and a file name in // Forgejo's flash cookie, which only Forgejo's fixes let through. for _, fixed := range []struct { request request result coreruleset.Result }{ {get("/alice/tools/wiki/search?q=python3"), matched(932260)}, {get("/admin/repos?q=python3"), matched(932260)}, { get("/alice/tools", "Cookie: flash=success%3DFile%2Bpackage.json%2Bdeleted"), matched(930120), }, } { wantResult(t, crs, withHost(fixed.request, forgejoHost), coreruleset.Result{}) wantResult(t, crs, withHost(fixed.request, otherHost), fixed.result) } } func TestHostnameListingForgejoGetsGiteasFixesToo(t *testing.T) { t.Parallel() crs := servingApps(t, map[string][]string{forgejoHost: {forgejo}}) for _, fixed := range giteaFixed { wantResult(t, crs, withHost(fixed.request, forgejoHost), coreruleset.Result{}) } } // forgejoSearches are Forgejo's searches whose q skips some rules: the // wiki's and the admin page's, from Forgejo's fixes, and the searches for a // name, from gitea's. // //nolint:gochecknoglobals // a constant cannot be a list var forgejoSearches = []string{ "/alice/tools/wiki/search", "/admin/repos", "/explore/repos", "/explore/users", "/alice/tools/branches", "/alice/-/packages", "/api/v1/packages/alice", "/alice", } // forgejoAttacks are an SQL injection, a path traversal, a script injection // and a shell command after a ";", as query values. // //nolint:gochecknoglobals // a constant cannot be a list var forgejoAttacks = []string{ sqlInjection, "..%2Fcustom%2Fconf%2Fapp.ini", scriptInjection, commandInjection, } func TestForgejoAttacksAreRefusedInItsSearches(t *testing.T) { t.Parallel() const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default defaults, bodies := atDefaults(t), readingBodies(t) for _, path := range forgejoSearches { for _, attack := range forgejoAttacks { if got := inspect(t, defaults, get(path+"?q="+attack)); got.Score < threshold { t.Errorf("GET %s?q=%s let through: %+v", path, attack, got) } // The same field in a form data body. r := request{http.MethodPost, path, []string{formData}} if got, _ := inspectBody(t, bodies, r, "q="+attack); got.Score < threshold { t.Errorf("POST %s q=%s let through: %+v", path, attack, got) } } } } func TestForgejoExclusionsAreNarrow(t *testing.T) { t.Parallel() const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default crs := atDefaults(t) // /bin/sh and the hard values, which q lets through on the wiki's // search, stay refused in q on paths near it and in another field on it. // /alice/wiki/search would be the code search of a repository named // wiki, where gitea's fixes let q through. Forgejo has no owner // named api, so /api/wiki/search is not one. for _, target := range []string{ "/alice/tools/wiki/Home?q=", "/alice/tools/wiki/search/x?q=", "/api/wiki/search?q=", "/alice/tools/wiki/search?other=", } { for _, value := range slices.Concat([]string{"%2Fbin%2Fsh"}, hardValues) { if got := inspect(t, crs, get(target+value)); got.Score < threshold { t.Errorf("%s%s let through: %+v", target, value, got) } } } }