check / check (push) Waiting to run
Every setting of waf and rproxy is read through viper, in place of the os.LookupEnv function Main passed down. NewViper turns AutomaticEnv and AllowEmptyEnv on, so a setting that is set but empty reads as set. A setting may also be given as a file, the _FILE form, which rproxy's settings now have too; rproxy still writes nothing to disk. The settings are renamed: waf's take the prefix SWW_WAF_, rproxy's take SWW_RPROXY_, and the shared SWW_COMMAND keeps SWW_. The Core Rule Set's settings take SWW_WAF_CORE_RULE_SET_ rather than SWW_WAF_WAF_. Model: deepseek-v4-pro (first version); opus-5-5 (rework)
183 lines
5.7 KiB
Go
183 lines
5.7 KiB
Go
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/milestones/developers",
|
|
"/org/acme/teams/-/search",
|
|
"/alice/-/projects",
|
|
"/alice/-/packages/generic/tools-cli/versions",
|
|
"/alice/-/packages/npm/@alice%2Ftools-cli/versions",
|
|
"/alice/tools/milestones",
|
|
"/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_CORE_RULE_SET_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_CORE_RULE_SET_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)
|
|
}
|
|
}
|
|
|
|
attacks := []string{"%2Fbin%2Fsh", commandInjection, pathTraversal}
|
|
|
|
// /bin/sh, a shell command after a ";", a path traversal and the hard
|
|
// values stay refused in q on paths near the searches, and in another
|
|
// field on them.
|
|
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/milestone/1?other=",
|
|
"/alice/tools/tags?other=",
|
|
"/org/acme/issues/developers?other=",
|
|
"/org/acme/teams/-/search?other=",
|
|
"/-/admin/users?other=",
|
|
"/-/admin/packages?other=",
|
|
} {
|
|
for _, value := range slices.Concat(attacks, hardValues) {
|
|
if got := inspect(t, crs, get(target+value)); got.Score < threshold {
|
|
t.Errorf("%s%s let through: %+v", target, value, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Under /admin/, where Forgejo serves its admin pages, only the hard
|
|
// values are left out: the tenth change lets them through in q on every
|
|
// path there, for Drupal.
|
|
for _, target := range []string{
|
|
"/admin/packages/delete?q=",
|
|
"/admin/packages?other=",
|
|
} {
|
|
for _, value := range attacks {
|
|
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
|
|
// searches of one team and of one milestone.
|
|
for _, path := range []string{
|
|
"/org/acme/issues/developers/x",
|
|
"/alice/tools/milestone/new",
|
|
} {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|