package coreruleset_test import ( "net/http" "slices" "testing" "sneak.berlin/go/smallwebwaf/internal/coreruleset" ) // forgejo is Forgejo's name in SWW_WAF_APPS. const forgejo = "forgejo" // forgejoHost is the hostname of the tests of SWW_WAF_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: those of // a wiki, of releases, of a job's log and of the admin page, from Forgejo's // fixes, and the searches for a name, from gitea's. The search of code at a // branch or tag is not one of them: there, as on gitea's search of code, q // lets these attacks through. // //nolint:gochecknoglobals // a constant cannot be a list var forgejoSearches = []string{ "/alice/tools/wiki/search", "/alice/tools/releases", "/api/v1/repos/alice/tools/releases", "/api/v1/repos/alice/tools/actions/jobs/1/logs", "/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_CORE_RULE_SET_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_CORE_RULE_SET_ANOMALY_THRESHOLD's default crs := atDefaults(t) // /bin/sh and the hard values, which q lets through on the searches // named in Forgejo's fixes and on those of branches and packages, stay // refused in q on paths near them and in another field on them. // /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 the paths under /api/ here are not searches. for _, target := range []string{ "/alice/tools/wiki/Home?q=", "/alice/tools/wiki/search/x?q=", "/api/wiki/search?q=", "/alice/tools/wiki/search?other=", "/alice/tools/search/branch?q=", "/alice/tools/search/commit/0123abc?q=", "/alice/tools/src/branch/main?q=", "/api/tools/search/branch/main?q=", "/alice/tools/search/branch/main?other=", "/alice/tools/releases/tag/v1.0?q=", "/alice/tools/releases/latest?q=", "/api/v1/releases?q=", "/alice/tools/releases?other=", "/api/v1/repos/alice/tools/actions/jobs/1?q=", "/api/v1/repos/alice/tools/actions/runs/1/logs?q=", "/alice/tools/actions/runs/1/jobs/0/logs?q=", "/api/v1/repos/alice/tools/actions/jobs/1/logs?other=", "/alice/tools/branches/x?q=", "/alice/-/packages/npm/x?q=", "/alice/tools/branches?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) } } } // Referer keeps its rules on a page opened from a path near the code // search at a branch or tag. for _, path := range []string{ "/alice/tools/search/branch", "/alice/tools/search/commit/0123abc", "/api/tools/search/branch/main", } { for _, attack := range []string{scriptTag, injection} { referer := "Referer: http://example.com" + path + "?q=" + attack if got := inspect(t, crs, get("/", referer)); got.Score < threshold { t.Errorf("%s let through: %+v", referer, got) } } } }