waf: decide every visitor request within rproxy's timeout (closes #394)
check / check (push) Failing after 3m43s

The Core Rule Set is the one check whose time a visitor can stretch.
At paranoia level 4 it took over 2 s on a request with an 8 KiB
request line, three 8 KiB Cookie lines and a 64 KiB form body.
SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE now takes at most 8K. The
Core Rule Set also reads no JSON body over 1 KiB, since Coraza turns
JSON into up to 8 times its size in names and values. That brings
such a request under 1 s.

A test per kind of input times the Core Rule Set on the worst request
within the limits, at the slowest settings. SPEC.md lists the limits
that bound it.

Model: opus-5-5
This commit is contained in:
sneak committed 2026-11-15 18:43:33 +00:00
1 parent 23788920f7
commit a2705dfa7a
10 files changed
+488 -85

No files matched your search

+40 -29
View File
@@ -390,22 +390,15 @@ func TestMoreParametersThanCorazaKeepsIsAMatch(t *testing.T) {
matched(900300),
)
// So it is with the fields of a form data or JSON body.
// So it is with the fields of a form data body. A JSON body the Core
// Rule Set reads, of at most mostJSONRead bytes, cannot hold that many.
crs = readingBodies(t)
for _, tc := range []struct{ header, body string }{
{formData, strings.Repeat("a=1&", 999) + attack},
{jsonBody, `{"a":[` + strings.Repeat("1,", 998) + `1],"id":"` + injection + `"}`},
} {
wantBody(t, crs, post(tc.header), tc.body, matched(942100), tc.body)
}
body := strings.Repeat("a=1&", 999) + attack
wantBody(t, crs, post(formData), body, matched(942100), body)
for _, tc := range []struct{ header, body string }{
{formData, strings.Repeat("a=1&", 1000) + attack},
{jsonBody, `{"a":[` + strings.Repeat("1,", 999) + `1],"id":"` + injection + `"}`},
} {
wantBody(t, crs, post(tc.header), tc.body, matched(900300), tc.body)
}
body = strings.Repeat("a=1&", 1000) + attack
wantBody(t, crs, post(formData), body, matched(900300), body)
}
func TestRedirectURIMayNameALocalAddress(t *testing.T) {
@@ -1512,13 +1505,15 @@ const (
wpTags = "/wp-json/wp/v2/tags"
)
// readingBodiesUpTo64K returns the Core Rule Set as sww runs it by default,
// readingBodiesUpTo8K returns the Core Rule Set as sww runs it by default,
// but reading bodies up to the largest SWW_WAF_CORE_RULE_SET_BODY_INSPECTION_SIZE.
func readingBodiesUpTo64K(t *testing.T) *coreruleset.CoreRuleSet {
func readingBodiesUpTo8K(t *testing.T) *coreruleset.CoreRuleSet {
t.Helper()
return shared(t, coreruleset.Params{
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyInspectionSize: 64<<10 - 1,
ParanoiaLevel: 1,
DisabledRules: defaultDisabledRules,
BodyInspectionSize: mostBodyRead,
})
}
@@ -1582,7 +1577,7 @@ func TestWordPressAttacksAreRefused(t *testing.T) {
const threshold = 5 // SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default
defaults, bodies := atDefaults(t), readingBodiesUpTo64K(t)
defaults, bodies := atDefaults(t), readingBodiesUpTo8K(t)
for _, attack := range []string{
sqlInjection,
@@ -1693,7 +1688,7 @@ func wordpressRESTAddresses(route string) []string {
func TestWordPressExclusionsFollowEveryRESTAddress(t *testing.T) {
t.Parallel()
crs := readingBodiesUpTo64K(t)
crs := readingBodiesUpTo8K(t)
content := `{"content":"` + wordpressBlock + `"}`
for _, route := range []string{
@@ -1919,7 +1914,7 @@ func TestWordPressCommandAfterASeparatorIsRefusedOnSearchesAndNames(
wantResult(t, crs, get(target+command), matched(932235))
}
bodies := readingBodiesUpTo64K(t)
bodies := readingBodiesUpTo8K(t)
for _, p := range wordpressBodyFields(command) {
r := request{http.MethodPost, p.target, []string{p.contentType}}
@@ -2143,7 +2138,7 @@ func TestWordPressBodyFieldsAreCheckedInTheQuery(t *testing.T) {
func TestWordPressBodyNamesAndAddressesAreExcludedOnlyThere(t *testing.T) {
t.Parallel()
crs := readingBodiesUpTo64K(t)
crs := readingBodiesUpTo8K(t)
for _, v := range wordpressWords() {
for _, field := range []string{"title", "excerpt", "name"} {
@@ -2262,7 +2257,7 @@ func TestWordPressExclusionsAreNarrow(t *testing.T) {
length = "Content-Length: 2"
)
crs := readingBodiesUpTo64K(t)
crs := readingBodiesUpTo8K(t)
saving := request{
http.MethodPost,
wpPost,
@@ -2473,19 +2468,35 @@ func TestFormDataAndMultipartAreReadUpToTheLimit(t *testing.T) {
func TestJSONAndXMLAreReadOnlyWhenNoLargerThanTheLimit(t *testing.T) {
t.Parallel()
crs := readingBodies(t)
// The limit is the body inspection size, but at most mostJSONRead for
// JSON. smallSize is a body inspection size under mostJSONRead.
const smallSize = 512
for _, tc := range []struct{ header, head, tail string }{
{jsonBody, `{"q":"` + injection + `","pad":"`, `"}`},
{xmlBody, "<r><q>" + injection + "</q><pad>", "</pad></r>"},
bodies, smallBodies := readingBodies(t), shared(t, coreruleset.Params{
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules,
BodyInspectionSize: smallSize,
})
jsonHead, jsonTail := `{"q":"`+injection+`","pad":"`, `"}`
for _, tc := range []struct {
crs *coreruleset.CoreRuleSet
header, head, tail string
limit int
}{
{bodies, jsonBody, jsonHead, jsonTail, mostJSONRead},
{smallBodies, jsonBody, jsonHead, jsonTail, smallSize},
{
bodies, xmlBody, "<r><q>" + injection + "</q><pad>", "</pad></r>",
bodyInspectionSize,
},
} {
fits := padded(tc.head, tc.tail, bodyInspectionSize)
wantBody(t, crs, post(tc.header), fits, matched(942100), fits)
fits := padded(tc.head, tc.tail, tc.limit)
wantBody(t, tc.crs, post(tc.header), fits, matched(942100), fits)
larger := padded(tc.head, tc.tail, bodyInspectionSize+1)
larger := padded(tc.head, tc.tail, tc.limit+1)
wantBody(
t,
crs,
tc.crs,
post(tc.header),
larger,
coreruleset.Result{},