check / check (push) Waiting to run
While SWW_WAF_BODY_LIMIT is set, these body fields skip only the rules that refused them, on their own paths: - tree_path, commit_summary and content on the web editor's pages: edit, new file, delete, upload, patch, cherry-pick and preview; - fields that name a branch, for Windows commands and command names: new_branch_name, old_branch_name, old_ref_name, from, to, head, base, branch and new_branch; base also when a pull request is edited through the API; - message of one file or several changed through the API, for system files and Windows commands. None applies when the query has a parameter of the same name. The requests were recorded from gitea 1.27.3 and Forgejo 17.0.1. Model: opus-5-5
285 lines
7.5 KiB
Go
285 lines
7.5 KiB
Go
//go:build recorded
|
|
|
|
// The tests that check the apps' recorded requests in testdata. The recorded
|
|
// tag keeps them out of the test phase's pass with the race detector, and
|
|
// its second pass, without the race detector, runs them.
|
|
|
|
package coreruleset_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
|
|
)
|
|
|
|
// appsUnset labels the checks made while SWW_APPS is unset.
|
|
const appsUnset = "SWW_APPS unset"
|
|
|
|
// giteaRequests returns the recorded ordinary gitea requests in testdata.
|
|
// Each has the Referer of a page that names the same, as a page opened
|
|
// from it has.
|
|
func giteaRequests(t *testing.T) []request {
|
|
t.Helper()
|
|
|
|
text, err := os.ReadFile(filepath.Join("testdata", "gitea-requests.txt"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var requests []request
|
|
|
|
for line := range strings.Lines(string(text)) {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
|
|
target, header, _ := strings.Cut(line, " ")
|
|
|
|
headers := []string{"Referer: http://git.example" + target}
|
|
if header != "" {
|
|
headers = append(headers, header)
|
|
}
|
|
|
|
requests = append(requests, get(target, headers...))
|
|
}
|
|
|
|
return requests
|
|
}
|
|
|
|
// TestOrdinaryGiteaRequestsPass checks gitea's recorded requests with
|
|
// SWW_APPS unset, and on giteaHost while SWW_APPS lists only gitea for it.
|
|
func TestOrdinaryGiteaRequestsPass(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
|
|
|
requests := giteaRequests(t)
|
|
giteaOnly := map[string][]string{giteaHost: {gitea}}
|
|
|
|
for apps, crs := range map[string]*coreruleset.CoreRuleSet{
|
|
appsUnset: atDefaults(t),
|
|
"SWW_APPS=" + giteaHost + ":" + gitea: servingApps(t, giteaOnly),
|
|
} {
|
|
for _, r := range requests {
|
|
got := inspect(t, crs, withHost(r, giteaHost))
|
|
if got.Score >= threshold {
|
|
t.Errorf(
|
|
"%s: %s %q refused: %+v",
|
|
apps,
|
|
r.target,
|
|
r.headers[1:],
|
|
got,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// recordedRequest is a line of a file of recorded requests in testdata.
|
|
type recordedRequest struct {
|
|
Method string `json:"method"`
|
|
Target string `json:"target"`
|
|
Headers []string `json:"headers"`
|
|
Body string `json:"body"`
|
|
}
|
|
|
|
// recordedRequests returns the recorded ordinary requests in the file name
|
|
// in testdata, each with its body.
|
|
func recordedRequests(t *testing.T, name string) []recordedRequest {
|
|
t.Helper()
|
|
|
|
path := filepath.Join("testdata", name)
|
|
|
|
text, err := os.ReadFile(path) //nolint:gosec // a file in testdata
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var requests []recordedRequest
|
|
|
|
for line := range strings.Lines(string(text)) {
|
|
if strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
|
|
var r recordedRequest
|
|
|
|
err := json.Unmarshal([]byte(line), &r)
|
|
if err != nil {
|
|
t.Fatalf("%q: %v", line, err)
|
|
}
|
|
|
|
requests = append(requests, r)
|
|
}
|
|
|
|
return requests
|
|
}
|
|
|
|
// wantRecordedRequestsPass checks that none of the recorded ordinary
|
|
// requests in the file name in testdata is refused: with SWW_APPS unset, and
|
|
// on the hostname where they were recorded, otherHost for WordPress and
|
|
// example.com for the other apps, while SWW_APPS lists only app for it. Both
|
|
// times, they are checked at the defaults and while bodies are read. A
|
|
// request with neither a body nor a Content-Type header is checked only at
|
|
// the defaults: with neither, the set that reads bodies has nothing more to
|
|
// check. Each request runs as its own parallel subtest for each setting of
|
|
// SWW_APPS.
|
|
func wantRecordedRequestsPass(t *testing.T, name, app string) {
|
|
t.Helper()
|
|
|
|
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
|
|
|
requests := recordedRequests(t, name)
|
|
|
|
for setting, apps := range map[string]map[string][]string{
|
|
appsUnset: nil,
|
|
"SWW_APPS lists only " + app: {"example.com": {app}, otherHost: {app}},
|
|
} {
|
|
defaults := servingApps(t, apps)
|
|
bodies := shared(t, coreruleset.Params{
|
|
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules,
|
|
BodyLimit: bodyLimit, Apps: apps,
|
|
})
|
|
|
|
for _, recorded := range requests {
|
|
t.Run(
|
|
setting+": "+recorded.Method+" "+recorded.Target,
|
|
func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := request{
|
|
recorded.Method,
|
|
recorded.Target,
|
|
recorded.Headers,
|
|
}
|
|
|
|
got, _ := inspectBody(t, defaults, r, recorded.Body)
|
|
if got.Score >= threshold {
|
|
t.Errorf(
|
|
"%s %s refused: %+v",
|
|
r.method,
|
|
r.target,
|
|
got,
|
|
)
|
|
}
|
|
|
|
typed := slices.ContainsFunc(
|
|
recorded.Headers,
|
|
func(header string) bool {
|
|
name, _, _ := strings.Cut(header, ":")
|
|
|
|
return strings.EqualFold(name, "Content-Type")
|
|
},
|
|
)
|
|
if recorded.Body == "" && !typed {
|
|
return
|
|
}
|
|
|
|
got, _ = inspectBody(t, bodies, r, recorded.Body)
|
|
if got.Score >= threshold {
|
|
t.Errorf(
|
|
"%s %s %.200q refused while bodies are read: %+v",
|
|
r.method,
|
|
r.target,
|
|
recorded.Body,
|
|
got,
|
|
)
|
|
}
|
|
},
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOrdinaryDrupalRequestsPass(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
wantRecordedRequestsPass(t, "drupal-requests.txt", "drupal")
|
|
}
|
|
|
|
// TestOrdinaryForgejoRequestsPass checks Forgejo's recorded requests at the
|
|
// defaults only: with SWW_APPS unset, and on example.com, where they were
|
|
// recorded, while SWW_APPS lists only forgejo for it. While bodies are read,
|
|
// the Core Rule Set still refuses a new wiki page whose title and text name
|
|
// commands and files, as it does such text in gitea's comments
|
|
// (TestGiteaBodiesTheCoreRuleSetRefuses). The requests that name a file or a
|
|
// branch in a body are checked while bodies are read in
|
|
// TestForgejoNamesInBodiesPass.
|
|
func TestOrdinaryForgejoRequestsPass(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
|
|
|
requests := recordedRequests(t, "forgejo-requests.txt")
|
|
forgejoOnly := map[string][]string{"example.com": {forgejo}}
|
|
|
|
for apps, crs := range map[string]*coreruleset.CoreRuleSet{
|
|
appsUnset: atDefaults(t),
|
|
"SWW_APPS=example.com:forgejo": servingApps(t, forgejoOnly),
|
|
} {
|
|
for _, recorded := range requests {
|
|
r := request{recorded.Method, recorded.Target, recorded.Headers}
|
|
if got, _ := inspectBody(t, crs, r, recorded.Body); got.Score >= threshold {
|
|
t.Errorf(
|
|
"%s: %s %s refused: %+v",
|
|
apps,
|
|
r.method,
|
|
r.target,
|
|
got,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestGiteaNamesInBodiesPass checks gitea's recorded requests that name a
|
|
// file or a branch in a body, while bodies are read too.
|
|
func TestGiteaNamesInBodiesPass(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
wantRecordedRequestsPass(t, "gitea-names-in-bodies.txt", gitea)
|
|
}
|
|
|
|
// TestForgejoNamesInBodiesPass is TestGiteaNamesInBodiesPass for Forgejo.
|
|
func TestForgejoNamesInBodiesPass(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
wantRecordedRequestsPass(t, "forgejo-names-in-bodies.txt", forgejo)
|
|
}
|
|
|
|
func TestOrdinaryMastodonRequestsPass(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
wantRecordedRequestsPass(t, "mastodon-requests.txt", "mastodon")
|
|
}
|
|
|
|
func TestOrdinaryMediaWikiRequestsPass(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
wantRecordedRequestsPass(t, "mediawiki-requests.txt", mediawiki)
|
|
}
|
|
|
|
func TestOrdinaryNextcloudRequestsPass(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
wantRecordedRequestsPass(t, "nextcloud-requests.txt", nextcloud)
|
|
}
|
|
|
|
func TestOrdinaryVaultwardenRequestsPass(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
wantRecordedRequestsPass(t, "vaultwarden-requests.txt", "vaultwarden")
|
|
}
|
|
|
|
func TestOrdinaryWordPressRequestsPass(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
wantRecordedRequestsPass(t, "wordpress-requests.txt", wordpress)
|
|
}
|