Core Rule Set reads request bodies up to SWWAF_WAF_BODY_LIMIT (closes #116)
check / check (push) Waiting to run
check / check (push) Waiting to run
SWWAF_WAF_BODY_LIMIT (default off, at most 1G) has the Core Rule Set read form data and multipart up to the limit, the rest streaming on, and JSON and XML no larger than it, with text/json and the application and text types ending in +json or +xml. The part read is held for the app. A size or time limit met while reading ends the request. Content-Encoding is refused again on these kinds. A body Coraza cannot parse, or a multipart body failing its strict checks, adds 5, as does a multipart body the limit cuts in a part's headers before a colon or a line feed. Coraza is built with no_fs_access, so writes no file. Rule 900300 moves to phase 2. Judgement call: Content-Encoding is refused on a JSON or XML body too large to read, as SPEC.md allows. Model: opus-5-5
This commit is contained in:
@@ -244,14 +244,16 @@ type Config struct {
|
||||
// level, from 1 to 4 (SWWAF_WAF_PARANOIA_LEVEL), and
|
||||
// WAFAnomalyThreshold the anomaly score at which a request is a match
|
||||
// (SWWAF_WAF_ANOMALY_THRESHOLD), 0 while it is off. WAFDisabledRules
|
||||
// are the ids of its rules switched off (SWWAF_WAF_DISABLED_RULES), and
|
||||
// are the ids of its rules switched off (SWWAF_WAF_DISABLED_RULES),
|
||||
// WAFExemptPaths the path prefixes it does not inspect
|
||||
// (SWWAF_WAF_EXEMPT_PATHS).
|
||||
// (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
|
||||
// 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
|
||||
@@ -396,6 +398,7 @@ var (
|
||||
errNeedsDBPath = errors.New("it names the file to look clients up in")
|
||||
errDBPathUnused = errors.New("only file reads it")
|
||||
errNotOver4K = errors.New("is not a size of more than 4K, such as 32K")
|
||||
errOver1G = errors.New("is more than 1G, the most Coraza reads")
|
||||
errNotDurationAboveZero = errors.New(
|
||||
"is not a duration above zero, such as 1h or 7d")
|
||||
errNotNumberAboveZero = errors.New(
|
||||
@@ -559,6 +562,7 @@ func FromEnvironment(lookupEnv func(string) (string, bool)) (*Config, error) {
|
||||
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),
|
||||
TrapPaths: env.trapPaths("SWWAF_TRAP_PATHS"),
|
||||
ErrorBurstThreshold: env.count("SWWAF_ERROR_BURST_THRESHOLD", "30"),
|
||||
LogRemoteURL: env.logRemoteURL("SWWAF_LOG_REMOTE_URL"),
|
||||
@@ -764,6 +768,15 @@ func (e *environment) size(name, defaultValue string) int64 {
|
||||
return size
|
||||
}
|
||||
|
||||
// wafBodyLimit reads the setting that is the most of a request body the
|
||||
// Core Rule Set reads.
|
||||
func (e *environment) wafBodyLimit(name, defaultValue string) int64 {
|
||||
limit, err := parseWAFBodyLimit(e.value(name, defaultValue))
|
||||
e.check(name, err)
|
||||
|
||||
return limit
|
||||
}
|
||||
|
||||
// headerSize reads the setting that is the largest request line and
|
||||
// headers.
|
||||
func (e *environment) headerSize(name, defaultValue string) int64 {
|
||||
@@ -1432,6 +1445,22 @@ func parseHeaderSize(value string) (int64, error) {
|
||||
return size, nil
|
||||
}
|
||||
|
||||
// parseWAFBodyLimit reads the most of a request body the Core Rule Set
|
||||
// reads: a size as parseSize reads it, or off, but at most 1G, since
|
||||
// Coraza, which runs the Core Rule Set, refuses to load with more.
|
||||
func parseWAFBodyLimit(value string) (int64, error) {
|
||||
limit, err := parseSize(value)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if limit > gibibyte {
|
||||
return 0, fmt.Errorf("%q %w", value, errOver1G)
|
||||
}
|
||||
|
||||
return limit, nil
|
||||
}
|
||||
|
||||
// splitUnit splits a size into its number and the bytes its suffix
|
||||
// stands for.
|
||||
func splitUnit(value string) (string, int64) {
|
||||
|
||||
@@ -96,6 +96,7 @@ const (
|
||||
wafAnomalyThreshold = "SWWAF_WAF_ANOMALY_THRESHOLD"
|
||||
wafDisabledRules = "SWWAF_WAF_DISABLED_RULES"
|
||||
wafExemptPaths = "SWWAF_WAF_EXEMPT_PATHS"
|
||||
wafBodyLimit = "SWWAF_WAF_BODY_LIMIT"
|
||||
trapPaths = "SWWAF_TRAP_PATHS"
|
||||
errorBurstThreshold = "SWWAF_ERROR_BURST_THRESHOLD"
|
||||
logRemoteURL = "SWWAF_LOG_REMOTE_URL"
|
||||
@@ -580,15 +581,20 @@ func TestCoreRuleSetSettings(t *testing.T) {
|
||||
environment{
|
||||
wafMode: config.WAFModeDetect, wafParanoiaLevel: "4", wafAnomalyThreshold: "10",
|
||||
wafDisabledRules: "942100, 920350", wafExemptPaths: "/api/, /static/",
|
||||
wafBodyLimit: "128K",
|
||||
},
|
||||
config.Config{
|
||||
WAFMode: config.WAFModeDetect, WAFParanoiaLevel: 4, WAFAnomalyThreshold: 10,
|
||||
WAFDisabledRules: []int{942100, 920350},
|
||||
WAFExemptPaths: []string{"/api/", "/static/"},
|
||||
WAFBodyLimit: 128 << 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
environment{wafMode: off, wafAnomalyThreshold: off, wafDisabledRules: ""},
|
||||
environment{
|
||||
wafMode: off, wafAnomalyThreshold: off, wafDisabledRules: "",
|
||||
wafBodyLimit: off,
|
||||
},
|
||||
config.Config{
|
||||
WAFMode: config.WAFModeOff, WAFParanoiaLevel: 1, WAFAnomalyThreshold: 0,
|
||||
WAFDisabledRules: []int{}, WAFExemptPaths: []string{},
|
||||
@@ -601,6 +607,7 @@ func TestCoreRuleSetSettings(t *testing.T) {
|
||||
WAFMode: cfg.WAFMode, WAFParanoiaLevel: cfg.WAFParanoiaLevel,
|
||||
WAFAnomalyThreshold: cfg.WAFAnomalyThreshold,
|
||||
WAFDisabledRules: cfg.WAFDisabledRules, WAFExemptPaths: cfg.WAFExemptPaths,
|
||||
WAFBodyLimit: cfg.WAFBodyLimit,
|
||||
}
|
||||
if !reflect.DeepEqual(got, tc.want) {
|
||||
t.Errorf("%v gave\n%+v\nwant\n%+v", tc.env, got, tc.want)
|
||||
@@ -608,6 +615,15 @@ func TestCoreRuleSetSettings(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWAFBodyLimitOf1G(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := fromEnvironment(t, environment{wafBodyLimit: "1G"})
|
||||
if cfg.WAFBodyLimit != 1<<30 {
|
||||
t.Errorf("1G read as %d", cfg.WAFBodyLimit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidCoreRuleSetSettingStopsTheStart(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -636,16 +652,28 @@ func TestInvalidCoreRuleSetSettingStopsTheStart(t *testing.T) {
|
||||
`"-942100" is not the id of a Core Rule Set rule, ` +
|
||||
`a whole number such as 942100`,
|
||||
},
|
||||
// The paranoia level, the allowed methods, the headers refused, and
|
||||
// a request with more query parameters than Coraza keeps.
|
||||
// The paranoia level, the allowed methods, the headers refused, a
|
||||
// request with more query parameters than Coraza keeps, and a body
|
||||
// Coraza cannot parse or that fails its strict checks.
|
||||
{wafDisabledRules, "942100,900000", `"900000"` + setupRule},
|
||||
{wafDisabledRules, "942100,900200", `"900200"` + setupRule},
|
||||
{wafDisabledRules, "942100,900250", `"900250"` + setupRule},
|
||||
{wafDisabledRules, "942100,900300", `"900300"` + setupRule},
|
||||
{wafDisabledRules, "942100,900440", `"900440"` + setupRule},
|
||||
{wafDisabledRules, "942100,900450", `"900450"` + setupRule},
|
||||
{
|
||||
wafExemptPaths, "api/",
|
||||
`"api/" is not a path prefix starting with /, such as /assets/`,
|
||||
},
|
||||
{
|
||||
wafBodyLimit, "128KB",
|
||||
`"128KB" is not a size such as 512K, 100M or 5G, or off`,
|
||||
},
|
||||
{wafBodyLimit, "2G", `"2G" is more than 1G, the most Coraza reads`},
|
||||
{
|
||||
wafBodyLimit, "1073741825",
|
||||
`"1073741825" is more than 1G, the most Coraza reads`,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name+"="+tc.value, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -2403,6 +2431,7 @@ func TestLogsEachSettingWithItsValue(t *testing.T) {
|
||||
wafAnomalyThreshold: "5",
|
||||
wafDisabledRules: defaultWAFDisabledRules,
|
||||
wafExemptPaths: "",
|
||||
wafBodyLimit: off,
|
||||
trapPaths: "",
|
||||
errorBurstThreshold: "30",
|
||||
logRemoteURL: "",
|
||||
|
||||
Reference in New Issue
Block a user