Core Rule Set: WordPress's names and addresses pass on more fields (closes #245)
check / check (push) Successful in 4m55s
check / check (push) Successful in 4m55s
Each field skips only the rules its recorded requests set off: - page on all of wp-admin, and the JSON fields title, excerpt and name on /wp/v2/ routes: 930120, 932120, 932250 and 932260; - p, postId and slug on all of wp-admin, and the JSON slug on /wp/v2/ routes: 932120 and 932260, since WordPress writes these as slugs; - url on the block editor's link previews and embeds, and a commenter's url: 930120 and 932120. Every field keeps 932235. Add 61 requests recorded from WordPress 7.1.4 with these fields. Model: opus-5-5
This commit was merged in pull request #249.
This commit is contained in:
1 parent
3093ed8f0d
commit
9f98982446
5 files changed
+342
-35
No files matched your search
@@ -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},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -799,7 +862,7 @@ func TestWordPressSearchesAndFileNamesAreExcludedOnlyThere(t *testing.T) {
|
||||
// TestWordPressGroupsSkipRulesOnTheirFields checks each field of the three
|
||||
// groups in wordpress.conf at one target of its group, against the same
|
||||
// values in a field no group names, then one field at each other target of
|
||||
// the group.
|
||||
// the group, then the names in the first group that skip fewer rules.
|
||||
func TestWordPressGroupsSkipRulesOnTheirFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
[]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{})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -869,6 +923,19 @@ func TestWordPressGroupsSkipRulesOnTheirFields(t *testing.T) {
|
||||
} {
|
||||
wantResult(t, crs, get(target), coreruleset.Result{})
|
||||
}
|
||||
|
||||
// The names WordPress writes as slugs skip only 932120 and 932260.
|
||||
for _, field := range []string{"p", "postId", "slug"} {
|
||||
for _, v := range wordpressWords() {
|
||||
want := coreruleset.Result{}
|
||||
if v.rule == 930120 || v.rule == 932250 {
|
||||
want = matched(v.rule)
|
||||
}
|
||||
|
||||
target := "/wp-admin/options.php?" + field + "=" + url.QueryEscape(v.value)
|
||||
wantResult(t, crs, get(target), want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWordPressCommandAfterASeparatorIsRefusedOnSearchesAndNames(t *testing.T) {
|
||||
@@ -891,9 +958,34 @@ 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=",
|
||||
} {
|
||||
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)
|
||||
}
|
||||
|
||||
// The addresses of a link preview, an embed and a commenter's site keep
|
||||
// 932260 too.
|
||||
for _, target := range []string{
|
||||
"/wp-json/wp-block-editor/v1/url-details?url=",
|
||||
"/wp-json/oembed/1.0/proxy?url=",
|
||||
} {
|
||||
wantResult(t, crs, get(target+command), matched(932235, 932260))
|
||||
}
|
||||
|
||||
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,14 +1038,24 @@ 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))
|
||||
}
|
||||
}
|
||||
|
||||
// TestWordPressRESTFieldsAreExcludedOnlyThere checks search, term and the
|
||||
// header X-HTTP-Method-Override at the REST API's addresses.
|
||||
// TestWordPressRESTFieldsAreExcludedOnlyThere checks search, term, url and
|
||||
// the header X-HTTP-Method-Override at the REST API's addresses.
|
||||
func TestWordPressRESTFieldsAreExcludedOnlyThere(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -981,6 +1083,24 @@ func TestWordPressRESTFieldsAreExcludedOnlyThere(t *testing.T) {
|
||||
wantChange(t, crs, get(directory+"?term=ls+-la"),
|
||||
get("/wp-json/example/v1/things?term=ls+-la"), matched(932250))
|
||||
|
||||
// The block editor's link previews and embeds: url skips only 930120 and
|
||||
// 932120.
|
||||
for _, target := range []string{
|
||||
"/wp-json/wp-block-editor/v1/url-details?url=",
|
||||
"/?rest_route=/wp-block-editor/v1/url-details&url=",
|
||||
"/wp-json/oembed/1.0/proxy?url=",
|
||||
"/?rest_route=/oembed/1.0/proxy&url=",
|
||||
} {
|
||||
for _, v := range wordpressWords() {
|
||||
want := coreruleset.Result{}
|
||||
if v.rule == 932250 || v.rule == 932260 {
|
||||
want = matched(v.rule)
|
||||
}
|
||||
|
||||
wantResult(t, crs, get(target+url.QueryEscape(v.value)), want)
|
||||
}
|
||||
}
|
||||
|
||||
// The header X-HTTP-Method-Override at the address on the site root,
|
||||
// and off the REST API.
|
||||
headers := []string{jsonBody, "Content-Length: 2", wordpressOverride}
|
||||
@@ -1002,19 +1122,87 @@ 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=powershell"), matched(932120))
|
||||
}
|
||||
|
||||
// TestWordPressBodyNamesAndAddressesAreExcludedOnlyThere checks the JSON
|
||||
// fields of a REST API save, a plugin to install 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 _, field := range []string{"title", "excerpt", "name"} {
|
||||
body := fmt.Sprintf(`{%q:%q}`, field, v.value)
|
||||
|
||||
for _, address := range wordpressRESTAddresses("/wp/v2/posts/1") {
|
||||
r := request{http.MethodPost, address, []string{jsonBody, wordpressOverride}}
|
||||
wantBody(t, crs, r, body, coreruleset.Result{}, body)
|
||||
}
|
||||
|
||||
r := request{http.MethodPost, "/wp-json/example/v1/things", []string{jsonBody}}
|
||||
wantBody(t, crs, r, body, matched(v.rule), body)
|
||||
}
|
||||
|
||||
other := fmt.Sprintf(`{"description":%q}`, v.value)
|
||||
r := request{http.MethodPost, wpTags, []string{jsonBody}}
|
||||
wantBody(t, crs, r, other, matched(v.rule), other)
|
||||
}
|
||||
|
||||
// A post's slug and a plugin to install skip only 932120 and 932260.
|
||||
post := request{http.MethodPost, wpPost, []string{jsonBody, wordpressOverride}}
|
||||
install := request{http.MethodPost, wpAjax, []string{formData}}
|
||||
|
||||
for _, v := range wordpressWords() {
|
||||
want := coreruleset.Result{}
|
||||
if v.rule == 930120 || v.rule == 932250 {
|
||||
want = matched(v.rule)
|
||||
}
|
||||
|
||||
slug := fmt.Sprintf(`{"slug":%q}`, v.value)
|
||||
wantBody(t, crs, post, slug, want, slug)
|
||||
|
||||
plugin := "action=install-plugin&slug=" + url.QueryEscape(v.value)
|
||||
wantBody(t, crs, install, plugin, want, plugin)
|
||||
}
|
||||
|
||||
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 +1217,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}},
|
||||
|
||||
Reference in new issue
Block a user