Gitea setup: Core Rule Set blocks attacks, not ordinary Gitea traffic (closes #198)
check / check (push) Successful in 3m20s

The Gitea setup runs the Core Rule Set in block mode. Rules that refused
ordinary Gitea requests are switched off only where they misfired:

- deploy/gitea/crs-exclusions.conf, loaded through the new setting
  SWWAF_CORE_RULE_SET_EXCLUSIONS_FILE, covers Gitea's search, file-name
  and branch-name fields, Referer, and file paths;
- smallwebwaf's defaults gain a seventh change: the registry upload
  path's from parameter skips 932260.

A file that cannot be read, does not parse or names an id from 900000
to 900999 stops servewaf at start. internal/waf's tests and
script/gitea-setup send the recorded requests in
internal/waf/testdata/gitea-requests.txt and three attacks.

Model: opus-5-5
This commit is contained in:
2026-11-06 19:15:51 +00:00
parent 21cac70b50
commit 7e26fe37bc
17 changed files with 806 additions and 239 deletions
+122 -1
View File
@@ -5,6 +5,7 @@ import (
"net/http/httptest"
"net/netip"
"net/url"
"os"
"path/filepath"
"reflect"
"strconv"
@@ -119,7 +120,7 @@ func atDefaults(t *testing.T) *waf.CoreRuleSet {
}
// wantChange checks that crs lets through passes, a gitea request one of
// the six changes is for, and still finds result in refused, a request
// the seven changes is for, and still finds result in refused, a request
// like it that the change is not for.
func wantChange(
t *testing.T, crs *waf.CoreRuleSet, passes, refused request, result waf.Result,
@@ -362,6 +363,126 @@ func TestEachDisabledRuleIsSwitchedOff(t *testing.T) {
wantResult(t, newCoreRuleSet(t, 1, 920350, 911100), r, waf.Result{})
}
func TestImagePushThatReusesALayerIsNotCheckedForCommandNames(t *testing.T) {
t.Parallel()
crs := atDefaults(t)
reuse := "?mount=sha256:0000&from=owner/python3-app"
wantChange(t, crs, get("/v2/owner/app/blobs/uploads/"+reuse), get("/upload/"+reuse),
matched(932260))
// A command after the image's name is still refused.
wantResult(t, crs,
get("/v2/owner/app/blobs/uploads/?from=owner/app;cat%20/etc/passwd"),
matched(930120, 932160))
}
// withExclusions returns the Core Rule Set at the defaults, with the
// exclusions file at path.
func withExclusions(path string) (*waf.CoreRuleSet, error) {
return waf.New(waf.Params{
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, ExclusionsFile: path,
})
}
// exclusionsFile writes text to a file of exclusions and returns its path.
func exclusionsFile(t *testing.T, text string) string {
t.Helper()
path := filepath.Join(t.TempDir(), "exclusions.conf")
err := os.WriteFile(path, []byte(text), 0o600)
if err != nil {
t.Fatal(err)
}
return path
}
func TestExclusionsFileIsLoadedAfterTheCoreRuleSet(t *testing.T) {
t.Parallel()
crs, err := withExclusions(exclusionsFile(t,
"SecRuleUpdateTargetById 942100 \"!ARGS:id\"\n"))
if err != nil {
t.Fatalf("load the exclusions: %v", err)
}
wantChange(t, crs, get("/?id=1'%20OR%20'1'='1"), get("/?q=1'%20OR%20'1'='1"),
matched(942100))
}
func TestExclusionsFileThatCannotBeUsedIsAnError(t *testing.T) {
t.Parallel()
for _, path := range []string{
filepath.Join(t.TempDir(), "missing.conf"),
exclusionsFile(t, "SecNoSuchDirective On\n"),
// smallwebwaf's own rules may not be switched off or changed.
exclusionsFile(t, "SecRuleRemoveById 900300\n"),
exclusionsFile(t, "SecRuleUpdateTargetById 900500 \"!ARGS:x\"\n"),
} {
_, err := withExclusions(path)
if err == nil {
t.Errorf("%s loaded", path)
}
}
}
// giteaRequests returns the targets of the recorded ordinary gitea requests
// in testdata, which script/gitea-setup sends too.
func giteaRequests(t *testing.T) []string {
t.Helper()
text, err := os.ReadFile(filepath.Join("testdata", "gitea-requests.txt"))
if err != nil {
t.Fatal(err)
}
var targets []string
for line := range strings.Lines(string(text)) {
line = strings.TrimSpace(line)
if line != "" && !strings.HasPrefix(line, "#") {
targets = append(targets, line)
}
}
return targets
}
func TestGiteaExclusionsLetOrdinaryGiteaRequestsThroughAndRefuseAttacks(t *testing.T) {
t.Parallel()
crs, err := withExclusions(filepath.Join("..", "..", "deploy", "gitea",
"crs-exclusions.conf"))
if err != nil {
t.Fatalf("load gitea's exclusions: %v", err)
}
const threshold = 5 // SWWAF_WAF_ANOMALY_THRESHOLD's default
// Each with the Referer of a page that names the same, as a page
// opened from it has.
for _, target := range giteaRequests(t) {
r := get(target, "Referer: http://git.example"+target)
if got := inspect(t, crs, r); got.Score >= threshold {
t.Errorf("%s refused: %+v", target, got)
}
}
for _, attack := range []string{
"/owner/repo?id=1'%20OR%20'1'='1",
"/owner/repo?id=..%2Fcustom%2Fconf%2Fapp.ini",
"/owner/repo?id=%3Cscript%3Ealert(1)%3C%2Fscript%3E",
} {
if got := inspect(t, crs, get(attack)); got.Score < threshold {
t.Errorf("%s let through: %+v", attack, got)
}
}
}
// bodyLimit is SWWAF_WAF_BODY_LIMIT in the tests that read bodies.
const bodyLimit = 8 << 10