Gitea setup: Core Rule Set blocks attacks, not ordinary Gitea traffic (closes #198)
check / check (push) Waiting to run

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 before the Core Rule Set's rules,
  covers Gitea's search, file-name and branch-name fields, Referer, file
  paths, API attachment names and Actions artifact uploads;
- 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, in every mode. 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 20:17:07 +00:00
parent 21cac70b50
commit 866b948ec0
17 changed files with 1048 additions and 242 deletions
+23 -8
View File
@@ -221,12 +221,15 @@ type Config struct {
// WAFExemptPaths the path prefixes it does not inspect
// (SWWAF_WAF_EXEMPT_PATHS), and WAFBodyLimit the most of a request body
// it reads (SWWAF_WAF_BODY_LIMIT), 0 while it is off and it reads none.
WAFMode string
WAFParanoiaLevel int
WAFAnomalyThreshold int
WAFDisabledRules []int
WAFExemptPaths []string
WAFBodyLimit int64
// CoreRuleSetExclusionsFile is the path of the file of its exclusions
// (SWWAF_CORE_RULE_SET_EXCLUSIONS_FILE), "" while there is none.
WAFMode string
WAFParanoiaLevel int
WAFAnomalyThreshold int
WAFDisabledRules []int
WAFExemptPaths []string
WAFBodyLimit int64
CoreRuleSetExclusionsFile string
// TrapPaths are the paths a request for which is a clear sign of
// attack (SWWAF_TRAP_PATHS), each starting with / and without a ?.
TrapPaths []string
@@ -523,8 +526,10 @@ func FromEnvironment(lookupEnv func(string) (string, bool)) (*Config, error) {
WAFAnomalyThreshold: env.numberOrOff("SWWAF_WAF_ANOMALY_THRESHOLD", "5"),
WAFDisabledRules: env.ruleIDs("SWWAF_WAF_DISABLED_RULES",
"920340,920420,920440,920640,930130,930140"),
WAFExemptPaths: env.pathPrefixes("SWWAF_WAF_EXEMPT_PATHS", ""),
WAFBodyLimit: env.wafBodyLimit("SWWAF_WAF_BODY_LIMIT", off),
WAFExemptPaths: env.pathPrefixes("SWWAF_WAF_EXEMPT_PATHS", ""),
WAFBodyLimit: env.wafBodyLimit("SWWAF_WAF_BODY_LIMIT", off),
CoreRuleSetExclusionsFile: env.filePath(
"SWWAF_CORE_RULE_SET_EXCLUSIONS_FILE"),
TrapPaths: env.trapPaths("SWWAF_TRAP_PATHS"),
ErrorBurstThreshold: env.count("SWWAF_ERROR_BURST_THRESHOLD", "30"),
LogRemoteURL: env.logRemoteURL("SWWAF_LOG_REMOTE_URL"),
@@ -1189,6 +1194,16 @@ func (e *environment) logRemoteURL(name string) *url.URL {
return remote
}
// filePath reads a setting that is the path of a file, "" while it is
// unset. Its value names a file already, so, unlike the other settings,
// it has no _FILE form. Whoever uses the file reads it.
func (e *environment) filePath(name string) string {
path, _ := e.lookupEnv(name)
e.settings = append(e.settings, slog.String(name, path))
return path
}
// certificates reads a setting that is the path of a file of PEM
// certificates. Unset or empty, it is nil. Its value names a file
// already, so, unlike the other settings, it has no _FILE form.
+9 -5
View File
@@ -87,6 +87,7 @@ const (
wafDisabledRules = "SWWAF_WAF_DISABLED_RULES"
wafExemptPaths = "SWWAF_WAF_EXEMPT_PATHS"
wafBodyLimit = "SWWAF_WAF_BODY_LIMIT"
exclusionsFile = "SWWAF_CORE_RULE_SET_EXCLUSIONS_FILE"
trapPaths = "SWWAF_TRAP_PATHS"
errorBurstThreshold = "SWWAF_ERROR_BURST_THRESHOLD"
logRemoteURL = "SWWAF_LOG_REMOTE_URL"
@@ -539,13 +540,14 @@ func TestCoreRuleSetSettings(t *testing.T) {
environment{
wafMode: config.WAFModeDetect, wafParanoiaLevel: "4", wafAnomalyThreshold: "10",
wafDisabledRules: "942100, 920350", wafExemptPaths: "/api/, /static/",
wafBodyLimit: "32K",
wafBodyLimit: "32K", exclusionsFile: "/etc/smallwebwaf/crs-exclusions.conf",
},
config.Config{
WAFMode: config.WAFModeDetect, WAFParanoiaLevel: 4, WAFAnomalyThreshold: 10,
WAFDisabledRules: []int{942100, 920350},
WAFExemptPaths: []string{"/api/", "/static/"},
WAFBodyLimit: 32 << 10,
WAFDisabledRules: []int{942100, 920350},
WAFExemptPaths: []string{"/api/", "/static/"},
WAFBodyLimit: 32 << 10,
CoreRuleSetExclusionsFile: "/etc/smallwebwaf/crs-exclusions.conf",
},
},
{
@@ -565,7 +567,8 @@ func TestCoreRuleSetSettings(t *testing.T) {
WAFMode: cfg.WAFMode, WAFParanoiaLevel: cfg.WAFParanoiaLevel,
WAFAnomalyThreshold: cfg.WAFAnomalyThreshold,
WAFDisabledRules: cfg.WAFDisabledRules, WAFExemptPaths: cfg.WAFExemptPaths,
WAFBodyLimit: cfg.WAFBodyLimit,
WAFBodyLimit: cfg.WAFBodyLimit,
CoreRuleSetExclusionsFile: cfg.CoreRuleSetExclusionsFile,
}
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("%v gave\n%+v\nwant\n%+v", tc.env, got, tc.want)
@@ -2327,6 +2330,7 @@ func TestLogsEachSettingWithItsValue(t *testing.T) {
wafDisabledRules: defaultWAFDisabledRules,
wafExemptPaths: "",
wafBodyLimit: off,
exclusionsFile: "",
trapPaths: "",
errorBurstThreshold: "30",
logRemoteURL: "",