package coreruleset_test import ( "net/http" "slices" "testing" "sneak.berlin/go/smallwebwaf/internal/coreruleset" ) // giteaSearches are searches of the pages gitea and Forgejo share whose q // skips the lists of system files and command names, in the eighth change. // gitea serves its admin pages under /-/admin/ and Forgejo under /admin/. // //nolint:gochecknoglobals // a constant cannot be a list var giteaSearches = []string{ "/notifications/watching", "/org/acme/issues/developers", "/org/acme/pulls/developers", "/org/acme/milestones/developers", "/org/acme/teams/-/search", "/alice/-/projects", "/alice/-/packages/generic/tools-cli/versions", "/alice/tools/milestones", "/alice/tools/milestone/1", "/alice/tools/projects", "/alice/tools/issues/suggestions", "/alice/tools/settings/actions/runners", "/org/acme/settings/actions/runners", "/user/settings/actions/runners", "/-/admin/actions/runners", "/-/admin/users", "/-/admin/orgs", "/-/admin/repos", "/-/admin/repos/unadopted", "/-/admin/emails", "/-/admin/emails/activate", "/admin/actions/runners", "/admin/users", "/admin/repos/unadopted", "/admin/emails/activate", } // giteaTagAndPackageSearches are the searches of tags and packages, whose q // also lets /bin/sh through, since a tag or package name can hold a slash. // //nolint:gochecknoglobals // a constant cannot be a list var giteaTagAndPackageSearches = []string{ "/alice/tools/tags", "/alice/tools/packages", "/-/admin/packages", "/admin/packages", } func TestGiteaAdminFormsLetHardValuesThroughInTheBody(t *testing.T) { t.Parallel() crs := readingBodies(t) // The forms that adopt a repository and activate an email send the // admin page's search in q, as gitea 1.27.3 and Forgejo 17.0.1 do. for _, value := range hardValues { adopt := "id=alice%2Flegacy-site&action=adopt&q=" + value + "&page=1" for _, path := range []string{"/-/admin/repos/unadopted", "/admin/repos/unadopted"} { r := request{http.MethodPost, path, []string{formData}} wantBody(t, crs, r, adopt, coreruleset.Result{}, adopt) } activate := "sort=email&q=" + value + "&is_primary=&is_activated=" + "&uid=2&email=bob%40example.com&primary=1&activate=0" for _, path := range []string{"/-/admin/emails/activate", "/admin/emails/activate"} { r := request{http.MethodPost, path, []string{formData}} wantBody(t, crs, r, activate, coreruleset.Result{}, activate) } } } func TestGiteaAttacksAreRefusedInItsSearches(t *testing.T) { t.Parallel() const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default defaults, bodies := atDefaults(t), readingBodies(t) for _, path := range slices.Concat(giteaSearches, giteaTagAndPackageSearches) { for _, attack := range []string{ sqlInjection, pathTraversal, scriptInjection, commandInjection, } { 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 TestGiteaSearchExclusionsAreNarrow(t *testing.T) { t.Parallel() const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default crs := atDefaults(t) // /bin/sh stays refused in q on the searches that are not of tags or // packages. for _, path := range giteaSearches { if got := inspect(t, crs, get(path+"?q=%2Fbin%2Fsh")); got.Score < threshold { t.Errorf("%s?q=%%2Fbin%%2Fsh let through: %+v", path, got) } } // /bin/sh and the hard values stay refused in q on paths near the // searches, and in another field on them. Paths under /admin/ are left // out here: the tenth change lets the hard values through in q on all of // them, for Drupal. for _, target := range []string{ "/org/acme/teams?q=", "/org/acme/teams/-/search/x?q=", "/org/acme/issues/developers/x?q=", "/alice/-/projects/1?q=", "/alice/-/packages/generic/tools-cli/1.0.0?q=", "/alice/tools/milestones/new?q=", "/alice/tools/milestone/new?q=", "/alice/tools/projects/1?q=", "/alice/tools/packages/x?q=", "/alice/tools/tags/v1.0?q=", "/alice/tools/issues/1?q=", "/alice/tools/settings/actions?q=", "/user/settings/actions?q=", "/-/admin/users/1/edit?q=", "/-/admin/emails/delete?q=", "/-/admin/actions/runners/1?q=", "/-/admin/packages/delete?q=", "/alice/tools/milestones?other=", "/alice/tools/tags?other=", "/org/acme/teams/-/search?other=", "/-/admin/users?other=", "/-/admin/packages?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) } } } }