Core Rule Set: WordPress's ordinary requests pass by default (closes #235)
check / check (push) Failing after 2m31s
check / check (push) Failing after 2m31s
Add the ninth change: wordpress.conf switches off, per path and field, each rule that refused recorded WordPress 7.1.4 traffic. Add the recorded requests, a test at the defaults and reading bodies, and list WordPress in the README. Model: opus-5-5
This commit is contained in:
1 parent
aede434f6a
commit
6a2c1e4860
6 files changed
+597
-12
No files matched your search
+158
-1
@@ -1,6 +1,7 @@
|
||||
package waf_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
@@ -120,7 +121,7 @@ func atDefaults(t *testing.T) *waf.CoreRuleSet {
|
||||
}
|
||||
|
||||
// wantChange checks that crs lets through passes, a gitea request one of
|
||||
// the eight changes is for, and still finds result in refused, a request
|
||||
// the nine 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,
|
||||
@@ -550,6 +551,162 @@ func TestOrdinaryGiteaRequestsPassAndAttacksAreRefused(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// wpLogin is WordPress's sign-in path.
|
||||
const wpLogin = "/wp-login.php"
|
||||
|
||||
// recordedRequest is a line of a file of recorded requests in testdata.
|
||||
type recordedRequest struct {
|
||||
Method string `json:"method"`
|
||||
Target string `json:"target"`
|
||||
Headers []string `json:"headers"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
// wordpressRequests returns the recorded ordinary WordPress requests in
|
||||
// testdata, each with its body.
|
||||
func wordpressRequests(t *testing.T) []recordedRequest {
|
||||
t.Helper()
|
||||
|
||||
text, err := os.ReadFile(filepath.Join("testdata", "wordpress-requests.txt"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var requests []recordedRequest
|
||||
|
||||
for line := range strings.Lines(string(text)) {
|
||||
if strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
var r recordedRequest
|
||||
|
||||
err := json.Unmarshal([]byte(line), &r)
|
||||
if err != nil {
|
||||
t.Fatalf("%q: %v", line, err)
|
||||
}
|
||||
|
||||
requests = append(requests, r)
|
||||
}
|
||||
|
||||
return requests
|
||||
}
|
||||
|
||||
// readingBodiesUpTo64K returns the Core Rule Set as sww runs it by default,
|
||||
// but reading bodies up to the largest SWW_WAF_BODY_LIMIT.
|
||||
func readingBodiesUpTo64K(t *testing.T) *waf.CoreRuleSet {
|
||||
t.Helper()
|
||||
|
||||
crs, err := waf.New(waf.Params{
|
||||
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyLimit: 64<<10 - 1,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("load the Core Rule Set: %v", err)
|
||||
}
|
||||
|
||||
return crs
|
||||
}
|
||||
|
||||
func TestOrdinaryWordPressRequestsPassAndAttacksAreRefused(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||
|
||||
defaults, bodies := atDefaults(t), readingBodiesUpTo64K(t)
|
||||
|
||||
for _, recorded := range wordpressRequests(t) {
|
||||
r := request{recorded.Method, recorded.Target, recorded.Headers}
|
||||
|
||||
crsSets := []*waf.CoreRuleSet{defaults}
|
||||
if recorded.Body != "" {
|
||||
crsSets = append(crsSets, bodies)
|
||||
}
|
||||
|
||||
for _, crs := range crsSets {
|
||||
got, _ := inspectBody(t, crs, r, recorded.Body)
|
||||
if got.Score >= threshold {
|
||||
t.Errorf("%s %s %.200q refused: %+v", r.method, r.target, recorded.Body, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, attack := range []string{
|
||||
"1'%20OR%20'1'='1",
|
||||
"..%2F..%2Fwp-config.php",
|
||||
"%3Cscript%3Ealert(1)%3C%2Fscript%3E",
|
||||
} {
|
||||
for _, target := range []string{
|
||||
"/?id=" + attack,
|
||||
wpLogin + "?id=" + attack,
|
||||
"/wp-admin/admin-ajax.php?id=" + attack,
|
||||
"/wp-json/wp/v2/posts?id=" + attack,
|
||||
} {
|
||||
if got := inspect(t, defaults, get(target)); got.Score < threshold {
|
||||
t.Errorf("%s let through: %+v", target, got)
|
||||
}
|
||||
}
|
||||
|
||||
// In a body field no exclusion names, on the paths with exclusions.
|
||||
body := "id=" + attack
|
||||
for _, target := range []string{
|
||||
wpLogin,
|
||||
"/wp-comments-post.php",
|
||||
"/wp-admin/admin-ajax.php",
|
||||
"/wp-admin/post.php",
|
||||
"/wp-admin/options.php",
|
||||
} {
|
||||
got, _ := inspectBody(t, bodies,
|
||||
request{http.MethodPost, target, []string{formData}}, body)
|
||||
if got.Score < threshold {
|
||||
t.Errorf("POST %s %q let through: %+v", target, body, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWordPressExclusionsAreNarrow(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
query = "SELECT name FROM users WHERE id = 1;"
|
||||
save = "/wp-json/wp/v2/posts/1"
|
||||
offREST = "/api/posts/1"
|
||||
override = "X-HTTP-Method-Override: PUT"
|
||||
length = "Content-Length: 2"
|
||||
)
|
||||
|
||||
crs := readingBodiesUpTo64K(t)
|
||||
saving := request{http.MethodPost, save, []string{jsonBody, override}}
|
||||
|
||||
content := `{"content":"` + query + `"}`
|
||||
wantBody(t, crs, saving, content, waf.Result{}, content)
|
||||
|
||||
// The other fields of the post keep the rule, and so does the same field
|
||||
// off the REST API.
|
||||
title := `{"title":"` + query + `"}`
|
||||
wantBody(t, crs, saving, title, matched(942100), title)
|
||||
wantBody(t, crs, request{http.MethodPost, offREST, []string{jsonBody}}, content,
|
||||
matched(942100), content)
|
||||
|
||||
// The header X-HTTP-Method-Override is refused off the REST API.
|
||||
defaults := atDefaults(t)
|
||||
wantResult(t, defaults,
|
||||
request{http.MethodPost, save, []string{jsonBody, length, override}},
|
||||
waf.Result{})
|
||||
wantResult(t, defaults,
|
||||
request{http.MethodPost, offREST, []string{jsonBody, length, override}},
|
||||
matched(920450))
|
||||
|
||||
// Signing in: pwd alone skips the list of system files.
|
||||
signIn := request{http.MethodPost, wpLogin, []string{formData}}
|
||||
|
||||
body := "log=a&pwd=b%20%2Fetc%2Fhosts"
|
||||
wantBody(t, crs, signIn, body, waf.Result{}, body)
|
||||
|
||||
body = "log=b%20%2Fetc%2Fhosts&pwd=a"
|
||||
wantBody(t, crs, signIn, body, matched(930120), body)
|
||||
}
|
||||
|
||||
// bodyLimit is SWW_WAF_BODY_LIMIT in the tests that read bodies.
|
||||
const bodyLimit = 8 << 10
|
||||
|
||||
|
||||
Reference in new issue
Block a user