Core Rule Set: random passwords pass on the remaining password fields (closes #288) #294
No files matched your search
@@ -603,9 +603,12 @@ client's limits by tag. See "Request tags" in [`SPEC.md`](SPEC.md).
|
|||||||
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 random password, as a password manager makes one, gets through these forms:
|
- A random password, as a password manager makes one, gets through these forms:
|
||||||
- signing in, on WordPress, Drupal, MediaWiki and Nextcloud;
|
- signing in, signing up, and changing or resetting a password, on
|
||||||
- signing in, signing up, changing the password, and moving or deleting the
|
WordPress, Drupal, MediaWiki, Mastodon, Nextcloud and gitea;
|
||||||
account, on Mastodon.
|
- moving or deleting the account, on Mastodon;
|
||||||
|
- deleting the account, on gitea;
|
||||||
|
- the admin page, on Vaultwarden, whose apps send a hash of the master
|
||||||
|
password, not the password itself.
|
||||||
- `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
|
||||||
|
|||||||
@@ -858,6 +858,27 @@ The changes hold in front of every app, and no setting undoes them:
|
|||||||
- to list a directory's last commits.
|
- to list a directory's last commits.
|
||||||
- The `Content-Range` header of an artifact upload from
|
- The `Content-Range` header of an artifact upload from
|
||||||
`actions/upload-artifact@v3` (920450).
|
`actions/upload-artifact@v3` (920450).
|
||||||
|
- The passwords in a body, for each rule that random passwords set off. The
|
||||||
|
numbers are in `internal/coreruleset/gitea.conf`.
|
||||||
|
- `password` on these paths:
|
||||||
|
- `/user/login`, `/user/sign_up`, `/user/activate`;
|
||||||
|
- `/user/link_account_signin` and `/user/link_account_signup`;
|
||||||
|
- `/user/recover_account`, which resets a password;
|
||||||
|
- `/user/settings/change_password` and `/user/settings/account`;
|
||||||
|
- `/user/settings/account/delete`;
|
||||||
|
- `/-/admin/users/new` and `/-/admin/users/<id>/edit`, and the same
|
||||||
|
paths under `/admin/`.
|
||||||
|
- `retype` on `/user/sign_up`, `/user/link_account_signup`,
|
||||||
|
`/user/settings/change_password` and `/user/settings/account`.
|
||||||
|
- `old_password` on `/user/settings/account`.
|
||||||
|
- `password` in the JSON body of `/api/v1/admin/users` and each user
|
||||||
|
under it.
|
||||||
|
|
||||||
|
Each field listed skips these rules on every path listed, not only on
|
||||||
|
the paths given for it. None applies when the query has a parameter of
|
||||||
|
the same name. `' OR '1'='1`, `../../etc/passwd`, `; cat /etc/passwd`
|
||||||
|
and `<img src=x onerror=x()>` sent as the password reach gitea;
|
||||||
|
`<script>alert(1)</script>` and `javascript:alert(1)` are still refused.
|
||||||
|
|
||||||
Elsewhere these fields keep every rule. The paths are those of gitea at the
|
Elsewhere these fields keep every rule. The paths are those of gitea at the
|
||||||
site root. An app other than gitea that serves one of them must check these
|
site root. An app other than gitea that serves one of them must check these
|
||||||
@@ -933,6 +954,20 @@ The changes hold in front of every app, and no setting undoes them:
|
|||||||
`<script>alert(1)</script>` and `javascript:alert(1)` are still
|
`<script>alert(1)</script>` and `javascript:alert(1)` are still
|
||||||
refused.
|
refused.
|
||||||
|
|
||||||
|
- `pass1`, `pass2` and `post_password` on `/wp-login.php`, which reset a
|
||||||
|
password and sign in to a protected post, for the same rules.
|
||||||
|
- `pass1` and `pass2` on these pages, for the same rules:
|
||||||
|
- `/wp-admin/profile.php`;
|
||||||
|
- `/wp-admin/user-edit.php`;
|
||||||
|
- `/wp-admin/user-new.php`;
|
||||||
|
- `/wp-admin/network/user-edit.php` and
|
||||||
|
`/wp-admin/user/profile.php`, on a multisite install.
|
||||||
|
|
||||||
|
Each of `pass1`, `pass2` and `post_password` skips these rules on
|
||||||
|
all of these pages and on `/wp-login.php`.
|
||||||
|
|
||||||
|
- `password` in form data or JSON on the REST API's `/wp/v2/users` and
|
||||||
|
the user under it, for the same rules.
|
||||||
- `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).
|
||||||
@@ -1012,6 +1047,17 @@ The changes hold in front of every app, and no setting undoes them:
|
|||||||
`<img src=x onerror=x()>` sent as the password reach Drupal;
|
`<img src=x onerror=x()>` sent as the password reach Drupal;
|
||||||
`<script>alert(1)</script>` and `javascript:alert(1)` are still refused.
|
`<script>alert(1)</script>` and `javascript:alert(1)` are still refused.
|
||||||
|
|
||||||
|
- The other passwords, for the same rules:
|
||||||
|
- `pass[pass1]`, `pass[pass2]` and `current_pass` on `/user/register`,
|
||||||
|
`/user/<id>/edit` and `/admin/people/create`;
|
||||||
|
- `pass.value`, `pass.0.value` and `pass.0.existing` in the JSON body of
|
||||||
|
`/user/register` and `/user/<id>`;
|
||||||
|
- `data.attributes.pass.value` and `data.attributes.pass.existing` in
|
||||||
|
the JSON:API body of `/jsonapi/user/user` and each user under it.
|
||||||
|
|
||||||
|
Each field listed skips these rules on every path listed, not only on
|
||||||
|
the paths given for it.
|
||||||
|
|
||||||
- `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.
|
||||||
@@ -1091,9 +1137,13 @@ 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:
|
- The passwords when signing in, signing up and changing a password:
|
||||||
- `wpPassword` on MediaWiki's pages;
|
- `wpPassword` on MediaWiki's pages;
|
||||||
- `lgpassword` and `password` on `/api.php`.
|
- `password` and `retype` on `Special:ChangeCredentials`;
|
||||||
|
- `retype` on `Special:CreateAccount`;
|
||||||
|
- `retype` on `Special:UserLogin`, where a password reset sets the new
|
||||||
|
password;
|
||||||
|
- `lgpassword`, `password` and `retype` on `/api.php`.
|
||||||
|
|
||||||
They skip each rule that random passwords set off:
|
They skip each rule that random passwords set off:
|
||||||
|
|
||||||
@@ -1106,6 +1156,10 @@ The changes hold in front of every app, and no setting undoes them:
|
|||||||
`<img src=x onerror=x()>` sent as a password reach MediaWiki;
|
`<img src=x onerror=x()>` sent as a password reach MediaWiki;
|
||||||
`<script>alert(1)</script>` and `javascript:alert(1)` are still refused.
|
`<script>alert(1)</script>` and `javascript:alert(1)` are still refused.
|
||||||
|
|
||||||
|
The path must name the special page, or be `/index.php` with a `title`
|
||||||
|
that names it. Other apps use `password` and `retype` too, so on any
|
||||||
|
other page those two fields keep every rule.
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
@@ -1239,6 +1293,9 @@ The changes hold in front of every app, and no setting undoes them:
|
|||||||
- `account_migration[current_password]` and
|
- `account_migration[current_password]` and
|
||||||
`form_redirect[current_password]`;
|
`form_redirect[current_password]`;
|
||||||
- `form_delete_confirmation[password]`;
|
- `form_delete_confirmation[password]`;
|
||||||
|
- `form_challenge[current_password]`, which asks for the password again, on
|
||||||
|
`/auth/challenge`, `/settings/two_factor_authentication` and
|
||||||
|
`/settings/two_factor_authentication_methods`, and the paths under them;
|
||||||
- `password` on `/api/v1/accounts`, in form data or JSON.
|
- `password` on `/api/v1/accounts`, in form data or JSON.
|
||||||
|
|
||||||
They skip each rule that random passwords set off:
|
They skip each rule that random passwords set off:
|
||||||
@@ -1248,6 +1305,9 @@ The changes hold in front of every app, and no setting undoes them:
|
|||||||
933100, 933135, 933180, 933210, 941100, 941120, 941160, 941180, 942100,
|
933100, 933135, 933180, 933210, 941100, 941120, 941160, 941180, 942100,
|
||||||
942151, 942190, 942230, 942290, 942320, 942540, 942550, 944150.
|
942151, 942190, 942230, 942290, 942320, 942540, 942550, 944150.
|
||||||
|
|
||||||
|
Each field in the first two items skips these rules on every path in those
|
||||||
|
items.
|
||||||
|
|
||||||
`' OR '1'='1`, `../../etc/passwd`, `; cat /etc/passwd` and
|
`' OR '1'='1`, `../../etc/passwd`, `; cat /etc/passwd` and
|
||||||
`<img src=x onerror=x()>` sent as a password reach Mastodon;
|
`<img src=x onerror=x()>` sent as a password reach Mastodon;
|
||||||
`<script>alert(1)</script>` and `javascript:alert(1)` are still refused.
|
`<script>alert(1)</script>` and `javascript:alert(1)` are still refused.
|
||||||
@@ -1322,6 +1382,17 @@ The changes hold in front of every app, and no setting undoes them:
|
|||||||
`username` in the query of `/api/hibp/breach`, the data breach report, skips
|
`username` in the query of `/api/hibp/breach`, the data breach report, skips
|
||||||
930120, 932120 and 932260 too.
|
930120, 932120 and 932260 too.
|
||||||
|
|
||||||
|
`token` in the body of `/admin`, the admin token typed on the admin page,
|
||||||
|
skips each rule that random passwords set off. The numbers are in
|
||||||
|
`internal/coreruleset/vaultwarden.conf`. It applies only when the query has
|
||||||
|
no `token` parameter. `' OR '1'='1`, `../../etc/passwd`, `; cat /etc/passwd`
|
||||||
|
and `<img src=x onerror=x()>` sent as the token reach Vaultwarden;
|
||||||
|
`<script>alert(1)</script>` and `javascript:alert(1)` are still refused.
|
||||||
|
|
||||||
|
The apps hash the master password before sending it. So `password` on
|
||||||
|
`/identity/connect/token`, `masterPasswordHash` and `newMasterPasswordHash`
|
||||||
|
hold only base64 and need no exclusion.
|
||||||
|
|
||||||
The names of uploaded files skip 920120 on the paths that take attachments
|
The names of uploaded files skip 920120 on the paths that take attachments
|
||||||
and file sends:
|
and file sends:
|
||||||
- `/api/ciphers/<id>/attachment` and the paths under it;
|
- `/api/ciphers/<id>/attachment` and the paths under it;
|
||||||
@@ -1397,6 +1468,35 @@ The changes hold in front of every app, and no setting undoes them:
|
|||||||
as the password reach Nextcloud; `<script>alert(1)</script>` and
|
as the password reach Nextcloud; `<script>alert(1)</script>` and
|
||||||
`javascript:alert(1)` are still refused.
|
`javascript:alert(1)` are still refused.
|
||||||
|
|
||||||
|
The other passwords skip the same rules, in form data or JSON. None applies
|
||||||
|
when the query has a parameter of the same name.
|
||||||
|
- `password` on `/login/confirm`, `/lostpassword/set/<token>/<user>`,
|
||||||
|
`/settings/users/changepassword` and a public share's
|
||||||
|
`/s/<token>/authenticate/<page>`.
|
||||||
|
- `oldpassword` and `newpassword` on `/settings/personal/changepassword`.
|
||||||
|
- `recoveryPassword` on `/settings/users/changepassword`.
|
||||||
|
- `oldPassword` and `newPassword` on `/apps/user_ldap/renewpassword`, which
|
||||||
|
changes an expired password, and on
|
||||||
|
`/apps/encryption/ajax/updatePrivateKeyPassword`.
|
||||||
|
- `oldPassword`, `newPassword` and `confirmPassword` on
|
||||||
|
`/apps/encryption/ajax/changeRecoveryPassword`.
|
||||||
|
- `recoveryPassword` and `confirmPassword` on
|
||||||
|
`/apps/encryption/ajax/adminRecovery`.
|
||||||
|
- `password` on the OCS API's `person/check`, which checks a sign-in, and
|
||||||
|
`core/apppassword/confirm`, which confirms the password.
|
||||||
|
- `password` on the OCS API's `cloud/users` and the share API's
|
||||||
|
`apps/files_sharing/api/v1/shares`, each user or share under them, and a
|
||||||
|
share's `send-email`.
|
||||||
|
- `value` on the OCS API's `cloud/users/<id>` when the body's `key` is
|
||||||
|
`password`. A body with more than one `key`, or a `key` or `value` in the
|
||||||
|
query, gets none of this.
|
||||||
|
|
||||||
|
Each field listed but `value` skips these rules on every path listed, not
|
||||||
|
only on the paths given for it.
|
||||||
|
|
||||||
|
`password` on `/login/flow/apptoken` and `/login/v2/apptoken` holds a
|
||||||
|
generated app token, so it needs no exclusion.
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
@@ -588,6 +588,12 @@ const (
|
|||||||
// several apps take a password.
|
// several apps take a password.
|
||||||
const passwordField = "password="
|
const passwordField = "password="
|
||||||
|
|
||||||
|
// passwordKey is that field's name, without the "=".
|
||||||
|
const passwordKey = "password"
|
||||||
|
|
||||||
|
// retypeKey is the field in which apps take the same password again.
|
||||||
|
const retypeKey = "retype"
|
||||||
|
|
||||||
// hardValues are words that name commands and files, as a query or form
|
// hardValues are words that name commands and files, as a query or form
|
||||||
// value. An app's tests check that its fields let them through.
|
// value. An app's tests check that its fields let them through.
|
||||||
//
|
//
|
||||||
@@ -661,27 +667,45 @@ func randomPasswords() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// passwordBody returns a body of r's type, form data or JSON, that sends
|
// 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
|
// each of passwords in each of names. In JSON, a dot in a name nests the
|
||||||
// on its own, and keeps every value of a JSON key sent more than once.
|
// field: pass.0.value is {"pass":{"0":{"value":password}}}, which Coraza
|
||||||
func passwordBody(t *testing.T, r request, name string, passwords ...string) string {
|
// names as it names the same field sent in an array. 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, names []string, passwords ...string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
if r.headers[0] != jsonBody {
|
var fields []string
|
||||||
return formBody(name, passwords...)
|
|
||||||
}
|
|
||||||
|
|
||||||
values := make([]string, len(passwords))
|
for _, name := range names {
|
||||||
|
if r.headers[0] != jsonBody {
|
||||||
|
fields = append(fields, formBody(name, passwords...))
|
||||||
|
|
||||||
for i, password := range passwords {
|
continue
|
||||||
pair, err := json.Marshal(map[string]string{name: password})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
values[i] = strings.Trim(string(pair), "{}")
|
keys := strings.Split(name, ".")
|
||||||
|
|
||||||
|
for _, password := range passwords {
|
||||||
|
var value any = password
|
||||||
|
for i := len(keys) - 1; i > 0; i-- {
|
||||||
|
value = map[string]any{keys[i]: value}
|
||||||
|
}
|
||||||
|
|
||||||
|
pair, err := json.Marshal(map[string]any{keys[0]: value})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fields = append(fields, string(pair[1:len(pair)-1]))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "{" + strings.Join(values, ",") + "}"
|
if r.headers[0] != jsonBody {
|
||||||
|
return strings.Join(fields, "&")
|
||||||
|
}
|
||||||
|
|
||||||
|
return "{" + strings.Join(fields, ",") + "}"
|
||||||
}
|
}
|
||||||
|
|
||||||
// formBody returns a form data body that sends each of values in the field
|
// formBody returns a form data body that sends each of values in the field
|
||||||
@@ -795,8 +819,9 @@ func wantRandomPasswordsPass(t *testing.T, r request, name string) {
|
|||||||
|
|
||||||
crs := readingBodies(t)
|
crs := readingBodies(t)
|
||||||
misfiring := misfiringPasswords(t)
|
misfiring := misfiringPasswords(t)
|
||||||
|
names := []string{name}
|
||||||
|
|
||||||
body := passwordBody(t, r, name, misfiring...)
|
body := passwordBody(t, r, names, misfiring...)
|
||||||
if len(body) > bodyLimit {
|
if len(body) > bodyLimit {
|
||||||
t.Fatalf("a body of %d bytes is cut at bodyLimit", len(body))
|
t.Fatalf("a body of %d bytes is cut at bodyLimit", len(body))
|
||||||
}
|
}
|
||||||
@@ -806,7 +831,7 @@ func wantRandomPasswordsPass(t *testing.T, r request, name string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, password := range misfiring {
|
for _, password := range misfiring {
|
||||||
body := passwordBody(t, r, name, password)
|
body := passwordBody(t, r, names, password)
|
||||||
if got, _ := inspectBody(t, crs, r, body); got.Score >= threshold {
|
if got, _ := inspectBody(t, crs, r, body); got.Score >= threshold {
|
||||||
t.Errorf("%s %s %s %q refused: %+v", r.method, r.target, name,
|
t.Errorf("%s %s %s %q refused: %+v", r.method, r.target, name,
|
||||||
password, got)
|
password, got)
|
||||||
@@ -816,8 +841,9 @@ func wantRandomPasswordsPass(t *testing.T, r request, name string) {
|
|||||||
|
|
||||||
// wantPasswordAttacksPass checks that an SQL injection, a path traversal, a
|
// wantPasswordAttacksPass checks that an SQL injection, a path traversal, a
|
||||||
// shell command and a script in an event handler, each sent as the whole
|
// shell command and a script in an event handler, each sent as the whole
|
||||||
// password in the field name of r's body, pass while bodies are read.
|
// password in each of the fields names of r's body, pass while bodies are
|
||||||
func wantPasswordAttacksPass(t *testing.T, r request, name string) {
|
// read.
|
||||||
|
func wantPasswordAttacksPass(t *testing.T, r request, names ...string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||||
@@ -826,9 +852,9 @@ func wantPasswordAttacksPass(t *testing.T, r request, name string) {
|
|||||||
"' OR '1'='1", pathTraversal, "; cat /etc/passwd",
|
"' OR '1'='1", pathTraversal, "; cat /etc/passwd",
|
||||||
"<img src=x onerror=x()>",
|
"<img src=x onerror=x()>",
|
||||||
} {
|
} {
|
||||||
body := passwordBody(t, r, name, attack)
|
body := passwordBody(t, r, names, attack)
|
||||||
if got, _ := inspectBody(t, readingBodies(t), r, body); got.Score >= threshold {
|
if got, _ := inspectBody(t, readingBodies(t), r, body); got.Score >= threshold {
|
||||||
t.Errorf("%s %s %s %q refused: %+v", r.method, r.target, name,
|
t.Errorf("%s %s %v %q refused: %+v", r.method, r.target, names,
|
||||||
attack, got)
|
attack, got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -843,7 +869,7 @@ func wantPasswordAttacksRefused(t *testing.T, r request, name string) {
|
|||||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||||
|
|
||||||
for _, attack := range []string{scriptTag, "javascript:alert(1)"} {
|
for _, attack := range []string{scriptTag, "javascript:alert(1)"} {
|
||||||
body := passwordBody(t, r, name, attack)
|
body := passwordBody(t, r, []string{name}, attack)
|
||||||
if got, _ := inspectBody(t, readingBodies(t), r, body); got.Score < threshold {
|
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,
|
t.Errorf("%s %s %s %q let through: %+v", r.method, r.target, name,
|
||||||
attack, got)
|
attack, got)
|
||||||
@@ -851,6 +877,64 @@ func wantPasswordAttacksRefused(t *testing.T, r request, name string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// passwordFields names the password fields one form or JSON body sends to
|
||||||
|
// target. contentType is formData or jsonBody.
|
||||||
|
type passwordFields struct {
|
||||||
|
target, contentType string
|
||||||
|
names []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// wantPasswordFieldsPass checks that the random passwords pass in each field
|
||||||
|
// of fields, that the attacks that pass in a password pass in all of a
|
||||||
|
// target's fields sent together, and that a script tag or a javascript: link
|
||||||
|
// in any one field is still refused. Sending the attacks together keeps the
|
||||||
|
// test cheap: a body of them is refused if any one field checks them.
|
||||||
|
func wantPasswordFieldsPass(t *testing.T, fields []passwordFields) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
for _, sent := range fields {
|
||||||
|
t.Run(sent.target+" "+sent.contentType, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
r := request{http.MethodPost, sent.target, []string{sent.contentType}}
|
||||||
|
|
||||||
|
wantPasswordAttacksPass(t, r, sent.names...)
|
||||||
|
|
||||||
|
for _, name := range sent.names {
|
||||||
|
wantRandomPasswordsPass(t, r, name)
|
||||||
|
wantPasswordAttacksRefused(t, r, name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGiteaPasswordsPass(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// gitea's password fields, each sent to one of the paths that takes it.
|
||||||
|
fields := []passwordFields{
|
||||||
|
{"/user/login", formData, []string{passwordKey}},
|
||||||
|
{"/user/sign_up", formData, []string{passwordKey, retypeKey}},
|
||||||
|
{"/user/link_account_signin", formData, []string{passwordKey}},
|
||||||
|
{"/user/link_account_signup", formData, []string{passwordKey, retypeKey}},
|
||||||
|
{"/user/activate", formData, []string{passwordKey}},
|
||||||
|
{"/user/recover_account", formData, []string{passwordKey}},
|
||||||
|
{"/user/settings/change_password", formData, []string{passwordKey, retypeKey}},
|
||||||
|
{
|
||||||
|
"/user/settings/account", formData,
|
||||||
|
[]string{"old_password", passwordKey, retypeKey},
|
||||||
|
},
|
||||||
|
{"/user/settings/account/delete", formData, []string{passwordKey}},
|
||||||
|
{"/-/admin/users/new", formData, []string{passwordKey}},
|
||||||
|
{"/-/admin/users/1/edit", formData, []string{passwordKey}},
|
||||||
|
{"/admin/users/1/edit", formData, []string{passwordKey}},
|
||||||
|
{"/api/v1/admin/users", jsonBody, []string{passwordKey}},
|
||||||
|
{"/api/v1/admin/users/alice", jsonBody, []string{passwordKey}},
|
||||||
|
}
|
||||||
|
|
||||||
|
wantPasswordFieldsPass(t, fields)
|
||||||
|
}
|
||||||
|
|
||||||
func TestPasswordFieldsStayCheckedInTheQuery(t *testing.T) {
|
func TestPasswordFieldsStayCheckedInTheQuery(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -864,6 +948,24 @@ func TestPasswordFieldsStayCheckedInTheQuery(t *testing.T) {
|
|||||||
{nextcloudSignIn, passwordField},
|
{nextcloudSignIn, passwordField},
|
||||||
{mediawikiAPI, passwordField},
|
{mediawikiAPI, passwordField},
|
||||||
{mediawikiAPI, "lgpassword="},
|
{mediawikiAPI, "lgpassword="},
|
||||||
|
{mediawikiAPI, retypeKey + "="},
|
||||||
|
{mediawikiAccountChange, passwordField},
|
||||||
|
{mediawikiAccountChange, retypeKey + "="},
|
||||||
|
{mediawikiSignUp, retypeKey + "="},
|
||||||
|
{"/wiki/Special:UserLogin", retypeKey + "="},
|
||||||
|
{"/user/sign_up", passwordField},
|
||||||
|
{"/user/settings/account", "old_password="},
|
||||||
|
{wpLogin, "pass1="},
|
||||||
|
{wpLogin, "post_password="},
|
||||||
|
{"/wp-admin/profile.php", "pass2="},
|
||||||
|
{"/wp-json/wp/v2/users", passwordField},
|
||||||
|
{drupalRegister, "pass%5Bpass1%5D="},
|
||||||
|
{"/user/1/edit", "current_pass="},
|
||||||
|
{"/auth/challenge", "form_challenge%5Bcurrent_password%5D="},
|
||||||
|
{"/login/confirm", passwordField},
|
||||||
|
{"/settings/personal/changepassword", "newpassword="},
|
||||||
|
{"/apps/encryption/ajax/adminRecovery", "confirmPassword="},
|
||||||
|
{"/admin", "token="},
|
||||||
} {
|
} {
|
||||||
r := request{
|
r := request{
|
||||||
http.MethodPost, sent.path + "?" + sent.field + "'%20OR%20'1'='1",
|
http.MethodPost, sent.path + "?" + sent.field + "'%20OR%20'1'='1",
|
||||||
@@ -877,6 +979,35 @@ func TestPasswordFieldsStayCheckedInTheQuery(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The password exclusions apply in front of every app. So each field keeps
|
||||||
|
// every rule on a nearby path that its exclusion does not name.
|
||||||
|
func TestPasswordExclusionsAreNarrow(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||||
|
|
||||||
|
crs := readingBodies(t)
|
||||||
|
|
||||||
|
for _, sent := range []struct{ target, contentType, name string }{
|
||||||
|
{"/user/settings/profile", formData, passwordKey},
|
||||||
|
{"/api/v1/user/repos", jsonBody, passwordKey},
|
||||||
|
{"/admin/users/overview", formData, "token"},
|
||||||
|
{"/wp-admin/options.php", formData, "pass1"},
|
||||||
|
{"/wp-admin/post.php", formData, "post_password"},
|
||||||
|
{"/user/password", formData, "pass[pass1]"},
|
||||||
|
{"/settings/apps", formData, passwordKey},
|
||||||
|
{"/ocs/v2.php/apps/spreed/api/v4/room", jsonBody, passwordKey},
|
||||||
|
{"/settings/profile", formData, "form_challenge[current_password]"},
|
||||||
|
} {
|
||||||
|
r := request{http.MethodPost, sent.target, []string{sent.contentType}}
|
||||||
|
|
||||||
|
body := passwordBody(t, r, []string{sent.name}, "' OR '1'='1")
|
||||||
|
if got, _ := inspectBody(t, crs, r, body); got.Score < threshold {
|
||||||
|
t.Errorf("POST %s %s let through: %+v", sent.target, sent.name, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGiteaAttacksAreRefused(t *testing.T) {
|
func TestGiteaAttacksAreRefused(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -1503,6 +1634,25 @@ func TestWordPressPasswordsPass(t *testing.T) {
|
|||||||
wantPasswordAttacksRefused(t, signIn, "pwd")
|
wantPasswordAttacksRefused(t, signIn, "pwd")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWordPressPasswordFieldsPass(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
pass := []string{"pass1", "pass2"}
|
||||||
|
|
||||||
|
wantPasswordFieldsPass(t, []passwordFields{
|
||||||
|
{wpLogin + "?action=resetpass", formData, pass},
|
||||||
|
{wpLogin + "?action=postpass", formData, []string{"post_password"}},
|
||||||
|
{"/wp-admin/profile.php", formData, pass},
|
||||||
|
{"/wp-admin/user-edit.php?user_id=2", formData, pass},
|
||||||
|
{"/wp-admin/user-new.php", formData, pass},
|
||||||
|
{"/wp-admin/network/user-edit.php?user_id=2", formData, pass},
|
||||||
|
{"/wp-admin/user/profile.php", formData, pass},
|
||||||
|
{"/wp-json/wp/v2/users", jsonBody, []string{passwordKey}},
|
||||||
|
{"/wp-json/wp/v2/users/me", formData, []string{passwordKey}},
|
||||||
|
{"/index.php?rest_route=/wp/v2/users/2", jsonBody, []string{passwordKey}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestWordPressExclusionsAreNarrow(t *testing.T) {
|
func TestWordPressExclusionsAreNarrow(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -224,6 +224,52 @@ SecRule REQUEST_FILENAME "@rx ^(?:/[a-z]{2,3}(?:-[a-z]+)?)?/user/login$" \
|
|||||||
ctl:ruleRemoveTargetById=942550;ARGS:/^(?:json\.)?pass$/,\
|
ctl:ruleRemoveTargetById=942550;ARGS:/^(?:json\.)?pass$/,\
|
||||||
ctl:ruleRemoveTargetById=944150;ARGS:/^(?:json\.)?pass$/"
|
ctl:ruleRemoveTargetById=944150;ARGS:/^(?:json\.)?pass$/"
|
||||||
|
|
||||||
|
# The passwords when signing up, changing the password and adding a user skip
|
||||||
|
# the same rules:
|
||||||
|
# pass[pass1], pass[pass2], current_pass: the forms at /user/register,
|
||||||
|
# /user/<id>/edit and /admin/people/create
|
||||||
|
# json.pass.value, json.pass.0.value, json.pass.0.existing: a client signing up
|
||||||
|
# or changing the password in Drupal's JSON format
|
||||||
|
# json.data.attributes.pass.value, .existing: a JSON:API client doing the same
|
||||||
|
SecRule REQUEST_FILENAME "@rx ^(?:/[a-z]{2,3}(?:-[a-z]+)?)?/(?:user/register|user/[0-9]+(?:/edit)?|admin/people/create|jsonapi/user/user(?:/[^/]+)?)$" \
|
||||||
|
"id:900820,phase:1,pass,nolog,chain"
|
||||||
|
SecRule &ARGS_GET:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/ "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=920270;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=920540;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=921200;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930100;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930110;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930120;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932125;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932130;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932160;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932230;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932270;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932280;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932330;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932340;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932370;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932380;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933100;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933135;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933180;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933210;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941100;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941120;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941160;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941180;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942100;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942151;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942190;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942230;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942290;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942320;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942540;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942550;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/,\
|
||||||
|
ctl:ruleRemoveTargetById=944150;ARGS:/^(?:pass\[pass[12]\]|current_pass|json\.pass\.(?:value|0\.(?:value|existing))|json\.data\.attributes\.pass\.(?:value|existing))$/"
|
||||||
|
|
||||||
# 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
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ const drupalSignIn = "/user/login"
|
|||||||
// drupalJSONSignIn is Drupal's sign-in in its JSON format.
|
// drupalJSONSignIn is Drupal's sign-in in its JSON format.
|
||||||
const drupalJSONSignIn = "/user/login?_format=json"
|
const drupalJSONSignIn = "/user/login?_format=json"
|
||||||
|
|
||||||
|
// drupalRegister is Drupal's sign-up form.
|
||||||
|
const drupalRegister = "/user/register"
|
||||||
|
|
||||||
|
// drupalJSONRegister is Drupal's sign-up in its JSON format.
|
||||||
|
const drupalJSONRegister = "/user/register?_format=json"
|
||||||
|
|
||||||
// drupalJSONPasswordReset is Drupal's password reset in its JSON format.
|
// drupalJSONPasswordReset is Drupal's password reset in its JSON format.
|
||||||
const drupalJSONPasswordReset = "/user/password?_format=json"
|
const drupalJSONPasswordReset = "/user/password?_format=json"
|
||||||
|
|
||||||
@@ -105,7 +111,7 @@ func TestDrupalAttacksAreRefusedInTheExcludedBodyFields(t *testing.T) {
|
|||||||
{reply, drupalName + attack},
|
{reply, drupalName + attack},
|
||||||
{drupalSignIn, drupalName + attack},
|
{drupalSignIn, drupalName + attack},
|
||||||
{"/fr/user/login", drupalName + attack},
|
{"/fr/user/login", drupalName + attack},
|
||||||
{"/user/register", drupalName + attack},
|
{drupalRegister, drupalName + attack},
|
||||||
{drupalEditAccount, "mail=" + attack},
|
{drupalEditAccount, "mail=" + attack},
|
||||||
{drupalTagForm, "name%5B0%5D%5Bvalue%5D=" + attack},
|
{drupalTagForm, "name%5B0%5D%5Bvalue%5D=" + attack},
|
||||||
{drupalTagForm, "description%5B0%5D%5Bvalue%5D=" + attack},
|
{drupalTagForm, "description%5B0%5D%5Bvalue%5D=" + attack},
|
||||||
@@ -182,6 +188,28 @@ func TestDrupalPasswordPasses(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDrupalPasswordFieldsPass(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const value = "pass.0.value"
|
||||||
|
|
||||||
|
pass := []string{"pass[pass1]", "pass[pass2]"}
|
||||||
|
|
||||||
|
wantPasswordFieldsPass(t, []passwordFields{
|
||||||
|
{drupalRegister, formData, pass},
|
||||||
|
{"/fr/user/register", formData, pass},
|
||||||
|
{"/user/1/edit", formData, []string{"current_pass", pass[0], pass[1]}},
|
||||||
|
{"/admin/people/create", formData, pass},
|
||||||
|
{drupalJSONRegister, jsonBody, []string{value}},
|
||||||
|
{drupalJSONRegister, jsonBody, []string{"pass.value"}},
|
||||||
|
{"/user/1?_format=json", jsonBody, []string{value, "pass.0.existing"}},
|
||||||
|
{
|
||||||
|
"/jsonapi/user/user/86e2fbf4-0c01-4e2d-9c05-9a56e07f3e53", jsonBody,
|
||||||
|
[]string{"data.attributes.pass.value", "data.attributes.pass.existing"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestDrupalAttacksAreRefusedInTheJSONPasswordReset(t *testing.T) {
|
func TestDrupalAttacksAreRefusedInTheJSONPasswordReset(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -274,7 +302,7 @@ func TestDrupalCommandAttackIsRefusedInTheExcludedBodyFields(t *testing.T) {
|
|||||||
{drupalAddArticle, "field_tags%5Btarget_id%5D="},
|
{drupalAddArticle, "field_tags%5Btarget_id%5D="},
|
||||||
{drupalAddArticle, "field_image%5B0%5D%5Balt%5D="},
|
{drupalAddArticle, "field_image%5B0%5D%5Balt%5D="},
|
||||||
{drupalSignIn, drupalName},
|
{drupalSignIn, drupalName},
|
||||||
{"/user/register", drupalName},
|
{drupalRegister, drupalName},
|
||||||
{drupalEditAccount, "mail="},
|
{drupalEditAccount, "mail="},
|
||||||
{"/comment/reply/node/1/comment", drupalName},
|
{"/comment/reply/node/1/comment", drupalName},
|
||||||
{drupalTagForm, "name%5B0%5D%5Bvalue%5D="},
|
{drupalTagForm, "name%5B0%5D%5Bvalue%5D="},
|
||||||
@@ -467,7 +495,7 @@ func TestDrupalBodyFieldsStayCheckedInTheQuery(t *testing.T) {
|
|||||||
// on a path other than the password reset.
|
// on a path other than the password reset.
|
||||||
for _, target := range []string{
|
for _, target := range []string{
|
||||||
drupalJSONPasswordReset + "&json.name=python3",
|
drupalJSONPasswordReset + "&json.name=python3",
|
||||||
"/user/register?_format=json",
|
drupalJSONRegister,
|
||||||
} {
|
} {
|
||||||
r := request{http.MethodPost, target, []string{jsonBody}}
|
r := request{http.MethodPost, target, []string{jsonBody}}
|
||||||
|
|
||||||
@@ -480,7 +508,7 @@ func TestDrupalBodyFieldsStayCheckedInTheQuery(t *testing.T) {
|
|||||||
// So does the JSON field mail.
|
// So does the JSON field mail.
|
||||||
for _, target := range []string{
|
for _, target := range []string{
|
||||||
drupalJSONPasswordReset + "&json.mail=python3%40example.com",
|
drupalJSONPasswordReset + "&json.mail=python3%40example.com",
|
||||||
"/user/register?_format=json",
|
drupalJSONRegister,
|
||||||
} {
|
} {
|
||||||
r := request{http.MethodPost, target, []string{jsonBody}}
|
r := request{http.MethodPost, target, []string{jsonBody}}
|
||||||
|
|
||||||
|
|||||||
@@ -289,3 +289,50 @@ SecRule REQUEST_FILENAME \
|
|||||||
"@rx ^/api/actions_pipeline/_apis/pipelines/workflows/[0-9]+/artifacts/[^/]+/upload$" \
|
"@rx ^/api/actions_pipeline/_apis/pipelines/workflows/[0-9]+/artifacts/[^/]+/upload$" \
|
||||||
"id:900640,phase:1,pass,nolog,\
|
"id:900640,phase:1,pass,nolog,\
|
||||||
ctl:ruleRemoveTargetById=920450;REQUEST_HEADERS_NAMES:Content-Range"
|
ctl:ruleRemoveTargetById=920450;REQUEST_HEADERS_NAMES:Content-Range"
|
||||||
|
|
||||||
|
# The passwords skip each rule that random passwords set off:
|
||||||
|
# password: signing in, signing up, linking an account, activating an account,
|
||||||
|
# resetting or changing the password, deleting an account, and creating or
|
||||||
|
# editing a user on the admin pages
|
||||||
|
# retype: signing up, linking an account by signing up, and changing the
|
||||||
|
# password
|
||||||
|
# old_password: changing the password
|
||||||
|
# json.password: creating or editing a user through the admin API
|
||||||
|
SecRule REQUEST_FILENAME "@rx ^/user/(?:login|sign_up|link_account_signin|link_account_signup|activate|recover_account|settings/(?:change_password|account(?:/delete)?))$|^/(?:-/)?admin/users/(?:new|[0-9]+/edit)$|^/api/v1/admin/users(?:/[^/]+)?$" \
|
||||||
|
"id:900650,phase:1,pass,nolog,chain"
|
||||||
|
SecRule &ARGS_GET:/^(?:password|retype|old_password|json\.password)$/ "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=920270;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=920540;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=921200;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930100;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930110;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930120;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932125;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932130;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932160;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932230;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932270;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932280;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932330;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932340;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932370;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932380;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933100;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933135;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933180;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933210;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941100;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941120;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941160;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941180;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942100;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942151;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942190;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942230;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942290;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942320;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942540;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942550;ARGS:/^(?:password|retype|old_password|json\.password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=944150;ARGS:/^(?:password|retype|old_password|json\.password)$/"
|
||||||
@@ -176,45 +176,47 @@ 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.
|
# The passwords named above skip each rule that random passwords set off, and
|
||||||
SecRule REQUEST_FILENAME "@rx ^/(?:settings/(?:migration|delete)|auth)(?:/|$)" \
|
# so does form_challenge[current_password]: confirming the password at
|
||||||
|
# /auth/challenge, or on the two-factor settings pages that ask for it again.
|
||||||
|
SecRule REQUEST_FILENAME "@rx ^/(?:settings/(?:migration|delete|two_factor_authentication(?:_methods)?)|auth)(?:/|$)" \
|
||||||
"id:900965,phase:1,pass,nolog,chain"
|
"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" \
|
SecRule &ARGS_GET:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=920270;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=920540;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=921200;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=930100;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=930110;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=930120;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=932125;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=932130;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=932160;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=932230;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=932250;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=932260;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=932270;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=932280;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=932330;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=932340;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=932370;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=932380;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=933100;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=933135;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=933180;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=933210;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=941100;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=941120;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=941160;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=941180;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=942100;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=942151;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=942190;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=942230;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=942290;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=942320;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=942540;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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=942550;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[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\])$/"
|
ctl:ruleRemoveTargetById=944150;ARGS:/^(?:user\[(?:password|password_confirmation|current_password)\]|(?:account_migration|form_redirect|form_challenge)\[current_password\]|form_delete_confirmation\[password\])$/"
|
||||||
|
|
||||||
SecRule REQUEST_FILENAME "@streq /api/v1/accounts" \
|
SecRule REQUEST_FILENAME "@streq /api/v1/accounts" \
|
||||||
"id:900966,phase:1,pass,nolog,chain"
|
"id:900966,phase:1,pass,nolog,chain"
|
||||||
|
|||||||
@@ -325,6 +325,18 @@ func TestMastodonPasswordsPass(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMastodonPasswordConfirmationPasses(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
challenge := []string{"form_challenge[current_password]"}
|
||||||
|
|
||||||
|
wantPasswordFieldsPass(t, []passwordFields{
|
||||||
|
{"/auth/challenge", formData, challenge},
|
||||||
|
{"/settings/two_factor_authentication/otp_authentication", formData, challenge},
|
||||||
|
{"/settings/two_factor_authentication_methods/disable", formData, challenge},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestMastodonCommandAttackIsRefusedOutsideAPost(t *testing.T) {
|
func TestMastodonCommandAttackIsRefusedOutsideAPost(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,12 @@ 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.
|
# These passwords skip each rule random passwords set off, on MediaWiki's pages:
|
||||||
|
# wpPassword: signing in and signing up
|
||||||
|
# and on /api.php:
|
||||||
|
# lgpassword: signing in with action=login
|
||||||
|
# password, retype: action=clientlogin, createaccount and
|
||||||
|
# changeauthenticationdata
|
||||||
SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php(?:$|/.)|^/wiki/" \
|
SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php(?:$|/.)|^/wiki/" \
|
||||||
"id:900917,phase:1,pass,nolog,chain"
|
"id:900917,phase:1,pass,nolog,chain"
|
||||||
SecRule REQUEST_FILENAME|ARGS_GET_NAMES "@rx ^/(?:w/)?index\.php/.|^/wiki/|^title$" "chain"
|
SecRule REQUEST_FILENAME|ARGS_GET_NAMES "@rx ^/(?:w/)?index\.php/.|^/wiki/|^title$" "chain"
|
||||||
@@ -151,42 +156,131 @@ SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php(?:$|/.)|^/wiki/" \
|
|||||||
|
|
||||||
SecRule REQUEST_FILENAME "@rx ^/(?:w/)?api\.php$" \
|
SecRule REQUEST_FILENAME "@rx ^/(?:w/)?api\.php$" \
|
||||||
"id:900918,phase:1,pass,nolog,chain"
|
"id:900918,phase:1,pass,nolog,chain"
|
||||||
SecRule &ARGS_GET:/^(?:lgpassword|password)$/ "@eq 0" \
|
SecRule &ARGS_GET:/^(?:lgpassword|password|retype)$/ "@eq 0" \
|
||||||
"ctl:ruleRemoveTargetById=920270;ARGS:/^(?:lgpassword|password)$/,\
|
"ctl:ruleRemoveTargetById=920270;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=920540;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=920540;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=921200;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=921200;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=930100;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=930100;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=930110;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=930110;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=930120;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=930120;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=932125;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=932125;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=932130;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=932130;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=932160;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=932160;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=932230;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=932230;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=932270;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=932270;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=932280;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=932280;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=932330;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=932330;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=932340;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=932340;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=932370;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=932370;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=932380;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=932380;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=933100;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=933100;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=933135;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=933135;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=933180;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=933180;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=933210;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=933210;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=941100;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=941100;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=941120;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=941120;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=941160;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=941160;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=941180;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=941180;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=942100;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=942100;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=942151;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=942151;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=942190;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=942190;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=942230;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=942230;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=942290;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=942290;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=942320;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=942320;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=942540;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=942540;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=942550;ARGS:/^(?:lgpassword|password)$/,\
|
ctl:ruleRemoveTargetById=942550;ARGS:/^(?:lgpassword|password|retype)$/,\
|
||||||
ctl:ruleRemoveTargetById=944150;ARGS:/^(?:lgpassword|password)$/"
|
ctl:ruleRemoveTargetById=944150;ARGS:/^(?:lgpassword|password|retype)$/"
|
||||||
|
|
||||||
|
# Other apps use the names password and retype too, and these exclusions apply
|
||||||
|
# in front of every app. So these two fields skip the same rules only on the
|
||||||
|
# pages that take them:
|
||||||
|
# password, retype: changing the password at Special:ChangeCredentials
|
||||||
|
# retype: signing up at Special:CreateAccount
|
||||||
|
# retype: setting the new password after a reset at Special:UserLogin
|
||||||
|
# The path names the page, or the path is /index.php and title names it. A title
|
||||||
|
# on any other path does not count, so another app's paths under /index.php/
|
||||||
|
# keep every rule.
|
||||||
|
SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php$|^/(?:(?:w/)?index\.php|wiki)/Special:ChangeCredentials(?:/|$)" \
|
||||||
|
"id:900919,phase:1,pass,nolog,chain"
|
||||||
|
SecRule REQUEST_FILENAME|ARGS_GET:title "@rx ^(?:/(?:(?:w/)?index\.php|wiki)/)?Special:ChangeCredentials(?:/|$)" "chain"
|
||||||
|
SecRule &ARGS_GET:/^(?:password|retype)$/ "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=920270;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=920540;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=921200;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930100;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930110;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930120;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932125;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932130;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932160;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932230;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932270;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932280;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932330;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932340;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932370;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932380;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933100;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933135;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933180;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933210;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941100;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941120;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941160;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941180;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942100;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942151;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942190;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942230;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942290;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942320;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942540;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942550;ARGS:/^(?:password|retype)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=944150;ARGS:/^(?:password|retype)$/"
|
||||||
|
|
||||||
|
SecRule REQUEST_FILENAME "@rx ^/(?:w/)?index\.php$|^/(?:(?:w/)?index\.php|wiki)/Special:(?:CreateAccount|UserLogin)(?:/|$)" \
|
||||||
|
"id:900920,phase:1,pass,nolog,chain"
|
||||||
|
SecRule REQUEST_FILENAME|ARGS_GET:title "@rx ^(?:/(?:(?:w/)?index\.php|wiki)/)?Special:(?:CreateAccount|UserLogin)(?:/|$)" "chain"
|
||||||
|
SecRule &ARGS_GET:retype "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=920270;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=920540;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=921200;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=930100;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=930110;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=930120;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=932125;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=932130;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=932160;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=932230;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=932250;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=932270;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=932280;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=932330;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=932340;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=932370;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=932380;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=933100;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=933135;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=933180;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=933210;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=941100;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=941120;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=941160;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=941180;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=942100;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=942151;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=942190;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=942230;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=942290;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=942320;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=942540;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=942550;ARGS:retype,\
|
||||||
|
ctl:ruleRemoveTargetById=944150;ARGS:retype"
|
||||||
|
|
||||||
# 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
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ const mediawikiUpload = "/wiki/Special:Upload"
|
|||||||
// mediawikiSignIn is the address MediaWiki's sign-in form posts to.
|
// mediawikiSignIn is the address MediaWiki's sign-in form posts to.
|
||||||
const mediawikiSignIn = "/index.php?title=Special:UserLogin"
|
const mediawikiSignIn = "/index.php?title=Special:UserLogin"
|
||||||
|
|
||||||
|
// mediawikiAccountChange is the page that changes a password.
|
||||||
|
const mediawikiAccountChange = "/wiki/Special:ChangeCredentials"
|
||||||
|
|
||||||
|
// mediawikiSignUp is the page that creates an account.
|
||||||
|
const mediawikiSignUp = "/wiki/Special:CreateAccount"
|
||||||
|
|
||||||
// mediawikiMove is the address MediaWiki's form for moving a page posts to.
|
// mediawikiMove is the address MediaWiki's form for moving a page posts to.
|
||||||
const mediawikiMove = "/index.php?title=Special:MovePage&action=submit"
|
const mediawikiMove = "/index.php?title=Special:MovePage&action=submit"
|
||||||
|
|
||||||
@@ -236,6 +242,60 @@ func TestMediaWikiAttacksAreRefusedInTheExcludedBodyFields(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMediaWikiSignUpAndPasswordChangeFieldsPass(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// The form that changes a password posts to this subpage.
|
||||||
|
const subpage = "/MediaWiki%5CAuth%5CPasswordAuthenticationRequest"
|
||||||
|
|
||||||
|
change := []string{passwordKey, retypeKey}
|
||||||
|
retype := []string{retypeKey}
|
||||||
|
|
||||||
|
wantPasswordFieldsPass(t, []passwordFields{
|
||||||
|
{mediawikiSignUp, formData, retype},
|
||||||
|
{"/index.php/Special:CreateAccount", formData, retype},
|
||||||
|
{"/index.php?title=Special:CreateAccount&returnto=Main_Page", formData, retype},
|
||||||
|
{"/wiki/Special:UserLogin", formData, retype},
|
||||||
|
{mediawikiSignIn, formData, retype},
|
||||||
|
{mediawikiAccountChange, formData, change},
|
||||||
|
{mediawikiAccountChange + subpage, formData, change},
|
||||||
|
{"/index.php/Special:ChangeCredentials", formData, change},
|
||||||
|
{"/index.php?title=Special:ChangeCredentials" + subpage, formData, change},
|
||||||
|
{"/w/index.php?title=Special:ChangeCredentials", formData, change},
|
||||||
|
{mediawikiAPI, formData, retype},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other apps use the names password and retype too. So these fields keep every
|
||||||
|
// rule on any page that does not take them, whatever title the query names.
|
||||||
|
func TestMediaWikiPasswordFieldsStayCheckedOffTheirPages(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const appPath = "/index.php/apps/mail/api/accounts"
|
||||||
|
|
||||||
|
crs := readingBodies(t)
|
||||||
|
|
||||||
|
for _, sent := range []struct{ target, name string }{
|
||||||
|
{appPath, passwordKey},
|
||||||
|
{appPath, retypeKey},
|
||||||
|
{appPath + "?title=Special:ChangeCredentials", passwordKey},
|
||||||
|
{appPath + "?title=Special:CreateAccount", retypeKey},
|
||||||
|
{"/index.php?title=Main_Page", passwordKey},
|
||||||
|
{"/index.php/Main_Page", retypeKey},
|
||||||
|
{"/wiki/Main_Page", passwordKey},
|
||||||
|
{"/wiki/Special:ChangeCredentialsExample", passwordKey},
|
||||||
|
{mediawikiSignUp, passwordKey},
|
||||||
|
{"/index.php?title=Special:CreateAccount", passwordKey},
|
||||||
|
} {
|
||||||
|
r := request{http.MethodPost, sent.target, []string{formData}}
|
||||||
|
|
||||||
|
body := formBody(sent.name, "' OR '1'='1")
|
||||||
|
if got, _ := inspectBody(t, crs, r, body); got.Score < mediawikiThreshold {
|
||||||
|
t.Errorf("POST %s %s let through: %+v", sent.target, sent.name, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMediaWikiPasswordsPass(t *testing.T) {
|
func TestMediaWikiPasswordsPass(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -101,6 +101,101 @@ SecRule REQUEST_FILENAME "@rx ^/(?:index\.php/)?login$" \
|
|||||||
ctl:ruleRemoveTargetById=942550;ARGS:password,\
|
ctl:ruleRemoveTargetById=942550;ARGS:password,\
|
||||||
ctl:ruleRemoveTargetById=944150;ARGS:password"
|
ctl:ruleRemoveTargetById=944150;ARGS:password"
|
||||||
|
|
||||||
|
# The other passwords, in form data or JSON, skip the same rules:
|
||||||
|
# password: confirming the password, resetting it, checking a sign-in with
|
||||||
|
# person/check, a public share's password, setting a share's password or
|
||||||
|
# sending it by email, creating or editing a user, and an administrator
|
||||||
|
# changing a user's password
|
||||||
|
# oldpassword, newpassword: changing the password on the settings page
|
||||||
|
# oldPassword, newPassword: changing an expired LDAP password, and changing
|
||||||
|
# the password of the encryption app's private key or recovery key
|
||||||
|
# recoveryPassword: an administrator changing the password of an encrypted
|
||||||
|
# account, or setting the encryption app's recovery key
|
||||||
|
# confirmPassword: the encryption app's recovery key, typed again
|
||||||
|
SecRule REQUEST_FILENAME "@rx ^/(?:index\.php/)?(?:login/confirm|lostpassword/set/[^/]+/[^/]+|settings/(?:personal|users)/changepassword|s/[^/]+/authenticate/[^/]+|apps/user_ldap/renewpassword|apps/encryption/ajax/(?:updatePrivateKeyPassword|changeRecoveryPassword|adminRecovery))$|^/ocs/v[12]\.php/(?:person/check|core/apppassword/confirm|cloud/users(?:/[^/]+)?|apps/files_sharing/api/v1/shares(?:/[^/]+(?:/send-email)?)?)$" \
|
||||||
|
"id:900126,phase:1,pass,nolog,chain"
|
||||||
|
SecRule &ARGS_GET:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/ "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=920270;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=920540;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=921200;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930100;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930110;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930120;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932125;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932130;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932160;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932230;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932270;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932280;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932330;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932340;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932370;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932380;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933100;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933135;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933180;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933210;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941100;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941120;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941160;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941180;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942100;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942151;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942190;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942230;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942290;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942320;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942540;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942550;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=944150;ARGS:/^(?:json\.)?(?:password|oldpassword|newpassword|recoverypassword|confirmpassword)$/"
|
||||||
|
|
||||||
|
# Editing a user through the OCS API can name the password as the field to
|
||||||
|
# change: key=password&value=..., or the same in JSON. This rule is in phase 2,
|
||||||
|
# because a phase 1 rule runs before the body is read. It checks that the body
|
||||||
|
# sends key once, so a second key cannot skip these rules for another field.
|
||||||
|
SecRule REQUEST_FILENAME "@rx ^/ocs/v[12]\.php/cloud/users/[^/]+$" \
|
||||||
|
"id:900127,phase:2,pass,nolog,chain"
|
||||||
|
SecRule &ARGS_GET:/^(?:json\.)?(?:key|value)$/ "@eq 0" "chain"
|
||||||
|
SecRule &ARGS_POST:/^(?:json\.)?key$/ "@eq 1" "chain"
|
||||||
|
SecRule ARGS_POST:/^(?:json\.)?key$/ "@streq password" \
|
||||||
|
"ctl:ruleRemoveTargetById=920270;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=920540;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=921200;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930100;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930110;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930120;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932125;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932130;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932160;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932230;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932270;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932280;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932330;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932340;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932370;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932380;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933100;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933135;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933180;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933210;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941100;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941120;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941160;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941180;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942100;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942151;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942190;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942230;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942290;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942320;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942540;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942550;ARGS:/^(?:json\.)?value$/,\
|
||||||
|
ctl:ruleRemoveTargetById=944150;ARGS:/^(?:json\.)?value$/"
|
||||||
|
|
||||||
# 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,\
|
||||||
|
|||||||
@@ -334,6 +334,111 @@ func TestNextcloudPasswordPasses(t *testing.T) {
|
|||||||
wantPasswordAttacksRefused(t, r, "password")
|
wantPasswordAttacksRefused(t, r, "password")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNextcloudPasswordFieldsPass(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
password := []string{passwordKey}
|
||||||
|
change := []string{"oldPassword", "newPassword"}
|
||||||
|
recovery := []string{"oldPassword", "newPassword", "confirmPassword"}
|
||||||
|
adminRecovery := []string{"recoveryPassword", "confirmPassword"}
|
||||||
|
|
||||||
|
wantPasswordFieldsPass(t, []passwordFields{
|
||||||
|
{"/ocs/v1.php/person/check", formData, password},
|
||||||
|
{"/ocs/v2.php/person/check", jsonBody, password},
|
||||||
|
{"/ocs/v2.php/core/apppassword/confirm", jsonBody, password},
|
||||||
|
{nextcloudShares + "/42/send-email", jsonBody, password},
|
||||||
|
{"/apps/user_ldap/renewpassword", formData, change},
|
||||||
|
{"/index.php/apps/user_ldap/renewpassword", formData, change},
|
||||||
|
{"/apps/encryption/ajax/updatePrivateKeyPassword", formData, change},
|
||||||
|
{"/index.php/apps/encryption/ajax/updatePrivateKeyPassword", formData, change},
|
||||||
|
{"/apps/encryption/ajax/changeRecoveryPassword", formData, recovery},
|
||||||
|
{"/index.php/apps/encryption/ajax/changeRecoveryPassword", formData, recovery},
|
||||||
|
{"/apps/encryption/ajax/adminRecovery", formData, adminRecovery},
|
||||||
|
{"/index.php/apps/encryption/ajax/adminRecovery", formData, adminRecovery},
|
||||||
|
{"/login/confirm", jsonBody, password},
|
||||||
|
{"/index.php/login/confirm", formData, password},
|
||||||
|
{"/lostpassword/set/AbCdEf/alice", jsonBody, password},
|
||||||
|
{
|
||||||
|
"/settings/personal/changepassword", jsonBody,
|
||||||
|
[]string{"oldpassword", "newpassword"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/index.php/settings/users/changepassword", formData,
|
||||||
|
[]string{passwordKey, "recoveryPassword"},
|
||||||
|
},
|
||||||
|
{nextcloudShare + "/authenticate/showShare", formData, password},
|
||||||
|
{nextcloudUsers, jsonBody, password},
|
||||||
|
{"/ocs/v1.php/cloud/users/alice", formData, password},
|
||||||
|
{nextcloudUsers + "/alice", jsonBody, password},
|
||||||
|
{nextcloudShares, jsonBody, password},
|
||||||
|
{nextcloudShares + "/42", formData, password},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNextcloudUserPasswordInValuePasses sends a password as Nextcloud's
|
||||||
|
// provisioning API takes it, as the value of the field the key names.
|
||||||
|
func TestNextcloudUserPasswordInValuePasses(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||||
|
|
||||||
|
crs := readingBodies(t)
|
||||||
|
target := nextcloudUsers + "/alice"
|
||||||
|
|
||||||
|
form := request{http.MethodPut, target, []string{formData}}
|
||||||
|
jsonRequest := request{http.MethodPut, target, []string{jsonBody}}
|
||||||
|
|
||||||
|
formPassword := func(passwords ...string) string {
|
||||||
|
return "key=password&" + formBody("value", passwords...)
|
||||||
|
}
|
||||||
|
jsonPassword := func(passwords ...string) string {
|
||||||
|
body := passwordBody(t, jsonRequest, []string{"value"}, passwords...)
|
||||||
|
|
||||||
|
return `{"key":"password",` + body[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
misfiring := misfiringPasswords(t)
|
||||||
|
|
||||||
|
for _, sent := range []struct {
|
||||||
|
r request
|
||||||
|
body string
|
||||||
|
}{
|
||||||
|
{form, formPassword(misfiring...)},
|
||||||
|
{jsonRequest, jsonPassword(misfiring...)},
|
||||||
|
{form, formPassword("' OR '1'='1", "<img src=x onerror=x()>")},
|
||||||
|
{jsonRequest, jsonPassword("; cat /etc/passwd")},
|
||||||
|
} {
|
||||||
|
if got, _ := inspectBody(t, crs, sent.r, sent.body); got.Score >= threshold {
|
||||||
|
t.Errorf("%s %.40s refused: %+v", sent.r.headers[0], sent.body, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any other key keeps every rule, and so does a second key.
|
||||||
|
for _, sent := range []struct {
|
||||||
|
r request
|
||||||
|
body string
|
||||||
|
}{
|
||||||
|
{form, "key=displayname&value=" + url.QueryEscape("' OR '1'='1")},
|
||||||
|
{form, formPassword(scriptTag)},
|
||||||
|
{form, "key=password&key=displayname&value=" + url.QueryEscape("' OR '1'='1")},
|
||||||
|
{jsonRequest, `{"key":"email","value":"' OR '1'='1"}`},
|
||||||
|
} {
|
||||||
|
if got, _ := inspectBody(t, crs, sent.r, sent.body); got.Score < threshold {
|
||||||
|
t.Errorf("%s %.40s let through: %+v", sent.r.headers[0], sent.body, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A value in the query keeps every rule.
|
||||||
|
r := request{
|
||||||
|
http.MethodPut, target + "?value=%27%20OR%20%271%27%3D%271",
|
||||||
|
[]string{formData},
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, _ := inspectBody(t, crs, r, "key=password"); got.Score < threshold {
|
||||||
|
t.Errorf("%s let through: %+v", r.target, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNextcloudSentencesPassInTheTexts(t *testing.T) {
|
func TestNextcloudSentencesPassInTheTexts(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -82,3 +82,45 @@ SecRule REQUEST_FILENAME "@rx ^/api/hibp/breach$" \
|
|||||||
SecRule REQUEST_FILENAME "@rx ^/api/ciphers/[0-9a-f-]{36}/attachment(?:-admin|/[0-9a-f]+(?:/share)?)?$|^/api/sends/[0-9a-f-]{36}/file/[0-9a-f]+$" \
|
SecRule REQUEST_FILENAME "@rx ^/api/ciphers/[0-9a-f-]{36}/attachment(?:-admin|/[0-9a-f]+(?:/share)?)?$|^/api/sends/[0-9a-f-]{36}/file/[0-9a-f]+$" \
|
||||||
"id:900153,phase:1,pass,nolog,\
|
"id:900153,phase:1,pass,nolog,\
|
||||||
ctl:ruleRemoveTargetById=920120;FILES"
|
ctl:ruleRemoveTargetById=920120;FILES"
|
||||||
|
|
||||||
|
# The admin token, which an administrator types at /admin, skips each rule that
|
||||||
|
# random passwords set off. The master password is hashed by the apps before
|
||||||
|
# they send it, so password and masterPasswordHash need no exclusion.
|
||||||
|
SecRule REQUEST_FILENAME "@rx ^/admin/?$" \
|
||||||
|
"id:900154,phase:1,pass,nolog,chain"
|
||||||
|
SecRule &ARGS_GET:token "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=920270;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=920540;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=921200;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=930100;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=930110;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=930120;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=932125;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=932130;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=932160;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=932230;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=932250;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=932270;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=932280;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=932330;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=932340;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=932370;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=932380;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=933100;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=933135;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=933180;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=933210;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=941100;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=941120;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=941160;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=941180;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=942100;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=942151;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=942190;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=942230;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=942290;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=942320;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=942540;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=942550;ARGS:token,\
|
||||||
|
ctl:ruleRemoveTargetById=944150;ARGS:token"
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
package coreruleset_test
|
package coreruleset_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"math/rand/v2"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -277,6 +279,73 @@ func vaultwardenUpload(name string) string {
|
|||||||
"\"\r\nContent-Type: application/octet-stream\r\n\r\nx\r\n" + end
|
"\"\r\nContent-Type: application/octet-stream\r\n\r\nx\r\n" + end
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestVaultwardenAdminTokenPasses(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
wantPasswordFieldsPass(t, []passwordFields{
|
||||||
|
{"/admin", formData, []string{"token"}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// masterPasswordHashes returns count random 32-byte hashes in base64, the
|
||||||
|
// form in which the apps send a master password.
|
||||||
|
func masterPasswordHashes(count int) []string {
|
||||||
|
const size, seed = 32, 288
|
||||||
|
|
||||||
|
random := rand.New(rand.NewPCG(seed, seed)) //nolint:gosec // a fixed sample
|
||||||
|
|
||||||
|
hashes := make([]string, count)
|
||||||
|
|
||||||
|
for i := range hashes {
|
||||||
|
hash := make([]byte, size)
|
||||||
|
for j := range hash {
|
||||||
|
hash[j] = byte(random.IntN(1 << 8)) //nolint:gosec // below 256
|
||||||
|
}
|
||||||
|
|
||||||
|
hashes[i] = base64.StdEncoding.EncodeToString(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hashes
|
||||||
|
}
|
||||||
|
|
||||||
|
// The apps hash the master password before they send it, so these fields
|
||||||
|
// hold only base64 and set off no rule, with no exclusion.
|
||||||
|
func TestVaultwardenMasterPasswordHashesPass(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const perBody = 20
|
||||||
|
|
||||||
|
crs := readingBodies(t)
|
||||||
|
hashes := masterPasswordHashes(perBody)
|
||||||
|
|
||||||
|
for _, sent := range []struct {
|
||||||
|
r request
|
||||||
|
names []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
request{http.MethodPost, "/identity/connect/token", []string{formData}},
|
||||||
|
[]string{passwordKey},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request{http.MethodPost, vaultwardenPassword, []string{jsonBody}},
|
||||||
|
[]string{"masterPasswordHash", "newMasterPasswordHash"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request{http.MethodPost, "/api/accounts/delete", []string{jsonBody}},
|
||||||
|
[]string{"masterPasswordHash"},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
body := passwordBody(t, sent.r, sent.names, hashes...)
|
||||||
|
if len(body) > bodyLimit {
|
||||||
|
t.Fatalf("a body of %d bytes is cut at bodyLimit", len(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, _ := inspectBody(t, crs, sent.r, body); len(got.RuleIDs) != 0 {
|
||||||
|
t.Errorf("POST %s %v set off rules: %+v", sent.r.target, sent.names, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestVaultwardenEncryptedFileNamesPassOnlyOnUploads(t *testing.T) {
|
func TestVaultwardenEncryptedFileNamesPassOnlyOnUploads(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -136,6 +136,92 @@ SecRule REQUEST_FILENAME "@streq /wp-login.php" \
|
|||||||
ctl:ruleRemoveTargetById=942550;ARGS:pwd,\
|
ctl:ruleRemoveTargetById=942550;ARGS:pwd,\
|
||||||
ctl:ruleRemoveTargetById=944150;ARGS:pwd"
|
ctl:ruleRemoveTargetById=944150;ARGS:pwd"
|
||||||
|
|
||||||
|
# The other passwords, which skip the same rules:
|
||||||
|
# pass1, pass2: resetting the password at /wp-login.php, changing it on the
|
||||||
|
# profile page and the page that edits a user, and adding a user; a
|
||||||
|
# multisite install also changes it on network/user-edit.php and
|
||||||
|
# user/profile.php
|
||||||
|
# post_password: the password of a protected post, sent to /wp-login.php
|
||||||
|
SecRule REQUEST_FILENAME "@rx ^/(?:wp-login\.php|wp-admin/(?:profile|user-edit|user-new|network/user-edit|user/profile)\.php)$" \
|
||||||
|
"id:900715,phase:1,pass,nolog,chain"
|
||||||
|
SecRule &ARGS_GET:/^(?:pass[12]|post_password)$/ "@eq 0" \
|
||||||
|
"ctl:ruleRemoveTargetById=920270;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=920540;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=921200;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930100;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930110;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=930120;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932125;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932130;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932160;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932230;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932250;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932260;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932270;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932280;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932330;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932340;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932370;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=932380;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933100;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933135;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933180;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=933210;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941100;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941120;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941160;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=941180;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942100;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942151;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942190;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942230;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942290;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942320;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942540;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=942550;ARGS:/^(?:pass[12]|post_password)$/,\
|
||||||
|
ctl:ruleRemoveTargetById=944150;ARGS:/^(?:pass[12]|post_password)$/"
|
||||||
|
|
||||||
|
# Creating or editing a user through the REST API, in password, in form data or
|
||||||
|
# JSON, which skips the same rules.
|
||||||
|
SecRule TX:rest_route "@rx ^/wp/v2/users(?:/(?:me|[0-9]+))?$" \
|
||||||
|
"id:900716,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$/"
|
||||||
|
|
||||||
# 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
|
||||||
# 932120 and 932260:
|
# 932120 and 932260:
|
||||||
|
|||||||
Reference in new issue
Block a user