Gitea setup: Core Rule Set blocks attacks, not ordinary Gitea traffic (closes #198)
check / check (push) Successful in 3m20s
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:
+122
-1
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user