Core Rule Set: WordPress's names and addresses pass on more fields (closes #245)
check / check (push) Successful in 4m35s

The four rules on system files and commands (930120, 932120, 932250,
932260) now skip:

- page, p, postId and slug on all of wp-admin;
- url on the block editor's link previews and embeds: the REST group
  now covers /wp-block-editor/v1/ and /oembed/1.0/;
- the JSON fields title, excerpt, slug and name on /wp/v2/ routes.

A commenter's url skips 930120 and 932120 only. Every field keeps 932235.
Add 61 requests recorded from WordPress 7.1.4 with these fields.

Model: opus-5-5
This commit is contained in:
clawbot committed 2026-11-09 19:35:08 +00:00
1 parent decd9b4658
commit 45deab094f
5 files changed
+272 -37

No files matched your search

+160 -21
View File
@@ -2,6 +2,7 @@ package coreruleset_test
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/netip"
@@ -564,8 +565,15 @@ func TestOrdinaryGiteaRequestsPassAndAttacksAreRefused(t *testing.T) {
}
}
// wpLogin is WordPress's sign-in path.
const wpLogin = "/wp-login.php"
// WordPress's paths: signing in, posting a comment, admin-ajax.php, and the
// REST API routes of a post and of the tags.
const (
wpLogin = "/wp-login.php"
wpComment = "/wp-comments-post.php"
wpAjax = "/wp-admin/admin-ajax.php"
wpPost = "/wp-json/wp/v2/posts/1"
wpTags = "/wp-json/wp/v2/tags"
)
// recordedRequest is a line of a file of recorded requests in testdata.
type recordedRequest struct {
@@ -665,6 +673,15 @@ func wordpressAttackTargets(attack string) []string {
"/wp-admin/plugin-editor.php?plugin=" + attack,
"/wp-admin/plugins.php?plugin=" + attack,
"/wp-admin/update.php?plugin=" + attack,
"/wp-admin/admin.php?page=" + attack,
"/wp-admin/options-general.php?page=" + attack,
"/wp-admin/tools.php?page=" + attack,
"/wp-admin/site-editor.php?p=" + attack,
"/wp-admin/site-editor.php?postId=" + attack,
"/wp-admin/admin-ajax.php?slug=" + attack,
"/wp-json/wp-block-editor/v1/url-details?url=" + attack,
"/wp-json/oembed/1.0/proxy?url=" + attack,
"/?rest_route=/oembed/1.0/proxy&url=" + attack,
}
}
@@ -706,8 +723,8 @@ func TestOrdinaryWordPressRequestsPassAndAttacksAreRefused(t *testing.T) {
body := "id=" + attack
for _, target := range []string{
wpLogin,
"/wp-comments-post.php",
"/wp-admin/admin-ajax.php",
wpComment,
wpAjax,
"/wp-admin/post.php",
"/wp-admin/options.php",
} {
@@ -717,6 +734,52 @@ func TestOrdinaryWordPressRequestsPassAndAttacksAreRefused(t *testing.T) {
t.Errorf("POST %s %q let through: %+v", target, body, got)
}
}
// In the body fields that skip some rules.
posts := append(wordpressBodyFields(attack),
wordpressBodyField{wpComment, formData, "url=" + attack})
for _, p := range posts {
got, _ := inspectBody(t, bodies,
request{http.MethodPost, p.target, []string{p.contentType}}, p.body)
if got.Score < threshold {
t.Errorf("POST %s %q let through: %+v", p.target, p.body, got)
}
}
}
}
// wordpressBodyField is a POST to target that sends body, a field whose
// name skips some rules there, with the Content-Type header contentType.
type wordpressBodyField struct{ target, contentType, body string }
// wordpressBodyFields returns encoded, a URL-encoded value, in a plugin to
// install and in each JSON field of a REST API save that skips some rules.
func wordpressBodyFields(encoded string) []wordpressBodyField {
value, _ := url.QueryUnescape(encoded)
return []wordpressBodyField{
{wpAjax, formData, "action=install-plugin&slug=" + encoded},
{wpPost, jsonBody, fmt.Sprintf(`{"title":%q}`, value)},
{wpPost, jsonBody, fmt.Sprintf(`{"excerpt":%q}`, value)},
{wpPost, jsonBody, fmt.Sprintf(`{"slug":%q}`, value)},
{wpTags, jsonBody, fmt.Sprintf(`{"name":%q}`, value)},
}
}
// wordpressWords returns words that ordinary WordPress names and searches
// hold, each with the rule it misfired on.
func wordpressWords() []struct {
value string
rule int
} {
return []struct {
value string
rule int
}{
{".htaccess", 930120},
{"powershell", 932120},
{"ls -la", 932250},
{"python3", 932260},
}
}
@@ -810,16 +873,6 @@ func TestWordPressGroupsSkipRulesOnTheirFields(t *testing.T) {
crs := atDefaults(t)
values := []struct {
value string
rule int
}{
{".htaccess", 930120},
{"powershell", 932120},
{"ls+-la", 932250},
{"python3", 932260},
}
for _, group := range []struct {
target string
fields []string
@@ -829,7 +882,7 @@ func TestWordPressGroupsSkipRulesOnTheirFields(t *testing.T) {
"/wp-admin/options.php?",
[]string{
"s", "search", "q", "term", "tag", "category_name", "plugin",
"checked%5B%5D", "theme", "stylesheet",
"checked%5B%5D", "theme", "stylesheet", "page", "p", "postId", "slug",
},
[]string{"url", "return", "redirect_to", "wp_http_referer", "_wp_http_referer"},
},
@@ -846,11 +899,12 @@ func TestWordPressGroupsSkipRulesOnTheirFields(t *testing.T) {
wantResult(t, crs, get(group.target+field+"="+pageAddress), coreruleset.Result{})
}
for _, v := range values {
wantResult(t, crs, get(group.target+"id="+v.value), matched(v.rule))
for _, v := range wordpressWords() {
value := url.QueryEscape(v.value)
wantResult(t, crs, get(group.target+"id="+value), matched(v.rule))
for _, field := range slices.Concat(group.fields, group.addressFields) {
wantResult(t, crs, get(group.target+field+"="+v.value), coreruleset.Result{})
wantResult(t, crs, get(group.target+field+"="+value), coreruleset.Result{})
}
}
}
@@ -863,6 +917,9 @@ func TestWordPressGroupsSkipRulesOnTheirFields(t *testing.T) {
"/?rest_route=/wp/v2/tags&slug=python3",
"/wp-json/oembed/1.0/embed?url=python3",
"/?rest_route=/oembed/1.0/embed&url=python3",
"/wp-json/oembed/1.0/proxy?url=python3",
"/wp-json/wp-block-editor/v1/url-details?url=python3",
"/?rest_route=/wp-block-editor/v1/url-details&url=python3",
"/index.php?tag=python3",
"/page/2/?tag=python3",
"/index.php/page/2/?s=python3",
@@ -891,9 +948,28 @@ func TestWordPressCommandAfterASeparatorIsRefusedOnSearchesAndNames(t *testing.T
"/?tag=",
"/wp-admin/edit.php?tag=",
"/wp-admin/admin-ajax.php?q=",
"/wp-admin/admin.php?page=",
"/wp-admin/options-general.php?page=",
"/wp-admin/tools.php?page=",
"/wp-admin/site-editor.php?p=",
"/wp-admin/site-editor.php?postId=",
"/wp-admin/admin-ajax.php?slug=",
"/wp-json/wp-block-editor/v1/url-details?url=",
"/wp-json/oembed/1.0/proxy?url=",
} {
wantResult(t, crs, get(target+command), matched(932235))
}
bodies := readingBodiesUpTo64K(t)
for _, p := range wordpressBodyFields(command) {
r := request{http.MethodPost, p.target, []string{p.contentType}}
wantBody(t, bodies, r, p.body, matched(932235), p.body)
}
// A commenter's address keeps 932260 too.
comment := request{http.MethodPost, wpComment, []string{formData}}
wantBody(t, bodies, comment, "url="+command, matched(932235, 932260), "url="+command)
}
// TestWordPressOtherFieldsAreExcludedOnlyThere checks each field and rule
@@ -946,6 +1022,16 @@ func TestWordPressOtherFieldsAreExcludedOnlyThere(t *testing.T) {
{"/wp-admin/plugin-editor.php?plugin=docker-helper", 932260},
{"/wp-admin/plugins.php?plugin=powershell-tools", 932120},
{"/wp-admin/update.php?plugin=docker-helper", 932260},
{"/wp-admin/admin.php?page=docker-helper", 932260},
{"/wp-admin/options-general.php?page=powershell-tools", 932120},
{"/wp-admin/tools.php?page=ls+-la", 932250},
{"/wp-admin/tools.php?page=.htaccess", 930120},
{"/wp-admin/site-editor.php?p=%2Fwp_template%2Fpython3-theme%2F%2Findex", 932260},
{"/wp-admin/site-editor.php?postId=powershell-theme%2F%2Findex", 932120},
{"/wp-admin/admin-ajax.php?slug=docker-helper", 932260},
{"/wp-json/wp-block-editor/v1/url-details?url=" +
"http%3A%2F%2Fblog.example%2Fpowershell-basics%2F", 932120},
{"/wp-json/oembed/1.0/proxy?url=http%3A%2F%2Fblog.example%2F.htaccess", 930120},
} {
_, query, _ := strings.Cut(tt.target, "?")
wantChange(t, crs, get(tt.target), get(elsewhere+"?"+query), matched(tt.rule))
@@ -1002,19 +1088,72 @@ func TestWordPressBodyFieldsAreCheckedInTheQuery(t *testing.T) {
wantResult(t, crs,
get("/wp-json/wp/v2/posts/1?json.content="+url.QueryEscape(wordpressBlock)),
matched(941180))
wantResult(t, crs, get("/wp-json/wp/v2/posts/1?json.title=python3"), matched(932260))
wantResult(t, crs, get("/wp-comments-post.php?url=python3"), matched(932260))
}
// TestWordPressBodyNamesAndAddressesAreExcludedOnlyThere checks the JSON
// fields of a REST API save and a commenter's address, against the same
// values in another field and off their paths.
func TestWordPressBodyNamesAndAddressesAreExcludedOnlyThere(t *testing.T) {
t.Parallel()
crs := readingBodiesUpTo64K(t)
for _, v := range wordpressWords() {
for _, p := range wordpressBodyFields(url.QueryEscape(v.value)) {
if p.contentType != jsonBody {
continue
}
for _, address := range wordpressRESTAddresses("/wp/v2/posts/1") {
r := request{http.MethodPost, address, []string{jsonBody, wordpressOverride}}
wantBody(t, crs, r, p.body, coreruleset.Result{}, p.body)
}
r := request{http.MethodPost, "/wp-json/example/v1/things", []string{jsonBody}}
wantBody(t, crs, r, p.body, matched(v.rule), p.body)
}
other := fmt.Sprintf(`{"description":%q}`, v.value)
r := request{http.MethodPost, wpTags, []string{jsonBody}}
wantBody(t, crs, r, other, matched(v.rule), other)
}
comment := request{http.MethodPost, wpComment, []string{formData}}
elsewhere := request{http.MethodPost, "/example/things", []string{formData}}
// A commenter's address skips the two rules that refused an address.
for _, v := range []struct {
address string
rule int
}{
{"http://blog.example/.htaccess", 930120},
{"http://blog.example/powershell-basics/", 932120},
} {
body := "url=" + url.QueryEscape(v.address)
wantBody(t, crs, comment, body, coreruleset.Result{}, body)
wantBody(t, crs, elsewhere, body, matched(v.rule), body)
author := "author=" + url.QueryEscape(v.address)
wantBody(t, crs, comment, author, matched(v.rule), author)
}
// The rules on Unix commands keep it.
wantBody(t, crs, comment, "url=python3", matched(932260), "url=python3")
wantBody(t, crs, comment, "url=ls+-la", matched(932250), "url=ls+-la")
}
func TestWordPressExclusionsAreNarrow(t *testing.T) {
t.Parallel()
const (
save = "/wp-json/wp/v2/posts/1"
offREST = "/api/posts/1"
length = "Content-Length: 2"
)
crs := readingBodiesUpTo64K(t)
saving := request{http.MethodPost, save, []string{jsonBody, wordpressOverride}}
saving := request{http.MethodPost, wpPost, []string{jsonBody, wordpressOverride}}
content := `{"content":"` + wordpressBlock + `"}`
wantBody(t, crs, saving, content, coreruleset.Result{}, content)
@@ -1029,7 +1168,7 @@ func TestWordPressExclusionsAreNarrow(t *testing.T) {
// 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, wordpressOverride}},
request{http.MethodPost, wpPost, []string{jsonBody, length, wordpressOverride}},
coreruleset.Result{})
wantResult(t, defaults,
request{http.MethodPost, offREST, []string{jsonBody, length, wordpressOverride}},