check / check (push) Successful in 6m41s
Adds drupal.conf, the ninth change: on Drupal's paths, the fields that carry a search, a name, a file name or an address skip the rules that refused ordinary requests there. The recorded requests from Drupal 11.4 pass at the defaults and while bodies are read; SQL injection, path traversal and script injection stay refused on those fields. Model: opus-5-5
220 lines
5.7 KiB
Go
220 lines
5.7 KiB
Go
package coreruleset_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// drupalRequest is a line of testdata/drupal-requests.txt.
|
|
type drupalRequest struct {
|
|
Method string `json:"method"`
|
|
Target string `json:"target"`
|
|
Headers []string `json:"headers"`
|
|
Body string `json:"body"`
|
|
}
|
|
|
|
// drupalRequests returns the recorded ordinary Drupal requests.
|
|
func drupalRequests(t *testing.T) []drupalRequest {
|
|
t.Helper()
|
|
|
|
text, err := os.ReadFile(filepath.Join("testdata", "drupal-requests.txt"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var requests []drupalRequest
|
|
|
|
for line := range strings.Lines(string(text)) {
|
|
if strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
|
|
var r drupalRequest
|
|
|
|
err := json.Unmarshal([]byte(line), &r)
|
|
if err != nil {
|
|
t.Fatalf("%q: %v", line, err)
|
|
}
|
|
|
|
requests = append(requests, r)
|
|
}
|
|
|
|
return requests
|
|
}
|
|
|
|
// drupalThreshold is SWW_WAF_ANOMALY_THRESHOLD's default.
|
|
const drupalThreshold = 5
|
|
|
|
// drupalAddArticle is Drupal's form for a new article.
|
|
const drupalAddArticle = "/node/add/article"
|
|
|
|
func TestOrdinaryDrupalRequestsPass(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
defaults, bodies := atDefaults(t), readingBodies(t)
|
|
|
|
for _, recorded := range drupalRequests(t) {
|
|
r := request{recorded.Method, recorded.Target, recorded.Headers}
|
|
|
|
got, _ := inspectBody(t, defaults, r, recorded.Body)
|
|
if got.Score >= drupalThreshold {
|
|
t.Errorf("%s %s refused: %+v", r.method, r.target, got)
|
|
}
|
|
|
|
got, _ = inspectBody(t, bodies, r, recorded.Body)
|
|
if got.Score >= drupalThreshold {
|
|
t.Errorf("%s %s %.200q refused while bodies are read: %+v",
|
|
r.method, r.target, recorded.Body, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// drupalAttacks are an SQL injection, a path traversal and a script
|
|
// injection, as a query value.
|
|
//
|
|
//nolint:gochecknoglobals // a constant cannot be a list
|
|
var drupalAttacks = []string{
|
|
"1'%20OR%20'1'='1",
|
|
"..%2F..%2Fsites%2Fdefault%2Fsettings.php",
|
|
"%3Cscript%3Ealert(1)%3C%2Fscript%3E",
|
|
}
|
|
|
|
func TestDrupalAttacksAreRefusedInTheExcludedQueryFields(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
crs := atDefaults(t)
|
|
|
|
for _, attack := range drupalAttacks {
|
|
for _, target := range []string{
|
|
"/?id=",
|
|
"/admin/content?id=",
|
|
"/admin/content?title=",
|
|
"/admin/content/media?name=",
|
|
"/admin/people?user=",
|
|
"/admin/content/files?filename=",
|
|
"/admin/content/files?filemime=",
|
|
"/admin/content/block?info=",
|
|
"/admin/content/comment?subject=",
|
|
"/admin/content/comment?author_name=",
|
|
"/admin/config/search/path?search=",
|
|
"/admin/content?destination=",
|
|
"/admin/foo?q=",
|
|
"/node/1/edit?destination=",
|
|
"/user/login?destination=",
|
|
"/search/node?keys=",
|
|
"/search?keys=",
|
|
"/entity_reference_autocomplete/user/default/KEY?q=",
|
|
"/jsonapi/node/article?filter%5Btitle%5D=",
|
|
"/jsonapi/node/article?filter%5Bf%5D%5Bcondition%5D%5Bvalue%5D=",
|
|
} {
|
|
if got := inspect(t, crs, get(target+attack)); got.Score < drupalThreshold {
|
|
t.Errorf("%s%s let through: %+v", target, attack, got)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDrupalAttacksAreRefusedInTheExcludedBodyFields(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
crs := readingBodies(t)
|
|
|
|
const reply = "/comment/reply/node/1/comment"
|
|
|
|
for _, attack := range drupalAttacks {
|
|
for _, sent := range []struct{ target, body string }{
|
|
{drupalAddArticle, "title%5B0%5D%5Bvalue%5D=" + attack},
|
|
{drupalAddArticle, "body%5B0%5D%5Bvalue%5D=" + attack},
|
|
{"/node/1/edit", "body%5B0%5D%5Bsummary%5D=" + attack},
|
|
{reply, "comment_body%5B0%5D%5Bvalue%5D=" + attack},
|
|
{reply, "subject%5B0%5D%5Bvalue%5D=" + attack},
|
|
{"/user/login", "pass=" + attack},
|
|
} {
|
|
r := request{http.MethodPost, sent.target, []string{formData}}
|
|
|
|
got, _ := inspectBody(t, crs, r, sent.body)
|
|
if got.Score < drupalThreshold {
|
|
t.Errorf("POST %s %q let through: %+v", sent.target, sent.body, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, attack := range []string{
|
|
`1' OR '1'='1`,
|
|
`<script>alert(1)</script>`,
|
|
} {
|
|
for _, field := range []string{
|
|
`"title":"` + attack + `"`,
|
|
`"body":{"value":"` + attack + `"}`,
|
|
} {
|
|
r := request{
|
|
http.MethodPost, "/jsonapi/node/article",
|
|
[]string{"Content-Type: application/vnd.api+json"},
|
|
}
|
|
body := `{"data":{"attributes":{` + field + `}}}`
|
|
|
|
got, _ := inspectBody(t, crs, r, body)
|
|
if got.Score < drupalThreshold {
|
|
t.Errorf("POST %s %s let through: %+v", r.target, body, got)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDrupalExclusionsAreNarrow(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
crs := atDefaults(t)
|
|
|
|
for _, passes := range []string{
|
|
"/admin/content?title=python3",
|
|
"/search/node?keys=python3",
|
|
"/entity_reference_autocomplete/user/default/KEY?q=python3",
|
|
"/jsonapi/node/article?filter%5Btitle%5D=python3",
|
|
"/node/1/edit?destination=python3",
|
|
} {
|
|
if got := inspect(t, crs, get(passes)); got.Score >= drupalThreshold {
|
|
t.Errorf("%s refused: %+v", passes, got)
|
|
}
|
|
}
|
|
|
|
for _, refused := range []string{
|
|
"/?title=python3",
|
|
"/admin/content?other=python3",
|
|
"/?keys=python3",
|
|
"/node/1?keys=python3",
|
|
"/?q=python3",
|
|
"/?filter%5Btitle%5D=python3",
|
|
"/python3-notes?destination=python3",
|
|
} {
|
|
if got := inspect(t, crs, get(refused)); got.Score < drupalThreshold {
|
|
t.Errorf("%s let through: %+v", refused, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDrupalBodyFieldsStayCheckedInTheQuery(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{ path, field string }{
|
|
{drupalAddArticle, "title%5B0%5D%5Bvalue%5D=python3"},
|
|
{drupalAddArticle, "body%5B0%5D%5Bvalue%5D=python3"},
|
|
{"/user/login", "pass=python3"},
|
|
} {
|
|
r := request{http.MethodPost, sent.path + "?" + sent.field, []string{formData}}
|
|
|
|
got, _ := inspectBody(t, crs, r, sent.field)
|
|
if got.Score < drupalThreshold {
|
|
t.Errorf("POST %s let through: %+v", r.target, got)
|
|
}
|
|
}
|
|
}
|