Author SHA1 Message Date
sneak 5600ba1255 Core Rule Set tests: load each set of settings once (closes #252)
check / check (push) Failing after 3m45s
Tests now share one Core Rule Set for each set of settings, loaded once
through sync.OnceValues in shared(). Exclusions-file tests and
TestCorazaWritesNoFile still load their own: loading is what they check.

Loading was not most of the time: the WordPress and Drupal recorded
requests, inspected one after another, were. Each recorded request now
runs as its own parallel subtest, with the same request and outcome.

Model: opus-5-5
2026-11-09 21:58:49 +00:00
2 changed files with 55 additions and 18 deletions

No files matched your search

+51 -18
View File
@@ -25,14 +25,36 @@ import (
//nolint:gochecknoglobals // a constant cannot be a list //nolint:gochecknoglobals // a constant cannot be a list
var defaultDisabledRules = []int{920340, 920420, 920440, 920640, 930130, 930140} var defaultDisabledRules = []int{920340, 920420, 920440, 920640, 930130, 930140}
// newCoreRuleSet returns the Core Rule Set at paranoia level level, with // loaded holds a loader of the Core Rule Set for each set of settings
// the rules in disabled switched off. // the tests have asked for, by the settings, so that each is loaded once.
func newCoreRuleSet(t *testing.T, level int, disabled ...int) *coreruleset.CoreRuleSet { //
//nolint:gochecknoglobals // the tests share it
var loaded = struct {
sync.Mutex
loaders map[string]func() (*coreruleset.CoreRuleSet, error)
}{loaders: map[string]func() (*coreruleset.CoreRuleSet, error){}}
// shared returns the Core Rule Set with params, loaded once for every
// test that asks for the same settings.
func shared(t *testing.T, params coreruleset.Params) *coreruleset.CoreRuleSet {
t.Helper() t.Helper()
crs, err := coreruleset.New(coreruleset.Params{ settings := fmt.Sprintf("%+v", params)
ParanoiaLevel: level, DisabledRules: disabled,
loaded.Lock()
load, found := loaded.loaders[settings]
if !found {
load = sync.OnceValues(func() (*coreruleset.CoreRuleSet, error) {
return coreruleset.New(params)
}) })
loaded.loaders[settings] = load
}
loaded.Unlock()
crs, err := load()
if err != nil { if err != nil {
t.Fatalf("load the Core Rule Set: %v", err) t.Fatalf("load the Core Rule Set: %v", err)
} }
@@ -40,6 +62,14 @@ func newCoreRuleSet(t *testing.T, level int, disabled ...int) *coreruleset.CoreR
return crs return crs
} }
// newCoreRuleSet returns the Core Rule Set at paranoia level level, with
// the rules in disabled switched off.
func newCoreRuleSet(t *testing.T, level int, disabled ...int) *coreruleset.CoreRuleSet {
t.Helper()
return shared(t, coreruleset.Params{ParanoiaLevel: level, DisabledRules: disabled})
}
// TestConcurrentRequestsKeepTheirOwnExclusions inspects a Drupal and a // TestConcurrentRequestsKeepTheirOwnExclusions inspects a Drupal and a
// WordPress request at once, many times over, on one Core Rule Set, as // WordPress request at once, many times over, on one Core Rule Set, as
// sww's server does. Each passes only while its own exclusions apply. // sww's server does. Each passes only while its own exclusions apply.
@@ -657,14 +687,9 @@ func recordedRequests(t *testing.T, name string) []recordedRequest {
func readingBodiesUpTo64K(t *testing.T) *coreruleset.CoreRuleSet { func readingBodiesUpTo64K(t *testing.T) *coreruleset.CoreRuleSet {
t.Helper() t.Helper()
crs, err := coreruleset.New(coreruleset.Params{ return shared(t, coreruleset.Params{
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyLimit: 64<<10 - 1, ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyLimit: 64<<10 - 1,
}) })
if err != nil {
t.Fatalf("load the Core Rule Set: %v", err)
}
return crs
} }
// wordpressAttackTargets returns attack in a query parameter no exclusion // wordpressAttackTargets returns attack in a query parameter no exclusion
@@ -730,6 +755,9 @@ func TestOrdinaryWordPressRequestsPassAndAttacksAreRefused(t *testing.T) {
defaults, bodies := atDefaults(t), readingBodiesUpTo64K(t) defaults, bodies := atDefaults(t), readingBodiesUpTo64K(t)
for _, recorded := range recordedRequests(t, "wordpress-requests.txt") { for _, recorded := range recordedRequests(t, "wordpress-requests.txt") {
t.Run(recorded.Method+" "+recorded.Target, func(t *testing.T) {
t.Parallel()
r := request{recorded.Method, recorded.Target, recorded.Headers} r := request{recorded.Method, recorded.Target, recorded.Headers}
crsSets := []*coreruleset.CoreRuleSet{defaults} crsSets := []*coreruleset.CoreRuleSet{defaults}
@@ -743,6 +771,7 @@ func TestOrdinaryWordPressRequestsPassAndAttacksAreRefused(t *testing.T) {
t.Errorf("%s %s %.200q refused: %+v", r.method, r.target, recorded.Body, got) t.Errorf("%s %s %.200q refused: %+v", r.method, r.target, recorded.Body, got)
} }
} }
})
} }
for _, attack := range []string{ for _, attack := range []string{
@@ -1289,14 +1318,9 @@ const injection = "1' OR '1'='1"
func readingBodies(t *testing.T) *coreruleset.CoreRuleSet { func readingBodies(t *testing.T) *coreruleset.CoreRuleSet {
t.Helper() t.Helper()
crs, err := coreruleset.New(coreruleset.Params{ return shared(t, coreruleset.Params{
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyLimit: bodyLimit, ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyLimit: bodyLimit,
}) })
if err != nil {
t.Fatalf("load the Core Rule Set: %v", err)
}
return crs
} }
// post is a POST request for / with a body of the type contentType, a // post is a POST request for / with a body of the type contentType, a
@@ -1581,10 +1605,19 @@ func TestCorazaWritesNoFile(t *testing.T) {
// refuses to load, and could not write a file of a multipart body. // refuses to load, and could not write a file of a multipart body.
t.Setenv("TMPDIR", filepath.Join(t.TempDir(), "missing")) t.Setenv("TMPDIR", filepath.Join(t.TempDir(), "missing"))
// The test loads its own Core Rule Set, since loading is part of what
// it checks.
crs, err := coreruleset.New(coreruleset.Params{
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyLimit: bodyLimit,
})
if err != nil {
t.Fatalf("load the Core Rule Set: %v", err)
}
body := "--b\r\nContent-Disposition: form-data; name=\"file\"; " + body := "--b\r\nContent-Disposition: form-data; name=\"file\"; " +
"filename=\"notes.txt\"\r\nContent-Type: text/plain\r\n\r\n" + "filename=\"notes.txt\"\r\nContent-Type: text/plain\r\n\r\n" +
strings.Repeat("a", 1000) + "\r\n" + field("q", injection) + end strings.Repeat("a", 1000) + "\r\n" + field("q", injection) + end
wantBody(t, readingBodies(t), post(multipart), body, matched(942100), body) wantBody(t, crs, post(multipart), body, matched(942100), body)
} }
func TestBodyLimitOf1GLoads(t *testing.T) { func TestBodyLimitOf1GLoads(t *testing.T) {
+4
View File
@@ -21,6 +21,9 @@ func TestOrdinaryDrupalRequestsPass(t *testing.T) {
defaults, bodies := atDefaults(t), readingBodies(t) defaults, bodies := atDefaults(t), readingBodies(t)
for _, recorded := range recordedRequests(t, "drupal-requests.txt") { for _, recorded := range recordedRequests(t, "drupal-requests.txt") {
t.Run(recorded.Method+" "+recorded.Target, func(t *testing.T) {
t.Parallel()
r := request{recorded.Method, recorded.Target, recorded.Headers} r := request{recorded.Method, recorded.Target, recorded.Headers}
got, _ := inspectBody(t, defaults, r, recorded.Body) got, _ := inspectBody(t, defaults, r, recorded.Body)
@@ -33,6 +36,7 @@ func TestOrdinaryDrupalRequestsPass(t *testing.T) {
t.Errorf("%s %s %.200q refused while bodies are read: %+v", t.Errorf("%s %s %.200q refused while bodies are read: %+v",
r.method, r.target, recorded.Body, got) r.method, r.target, recorded.Body, got)
} }
})
} }
} }