Core Rule Set: random passwords pass on the remaining password fields (closes #288)
check / check (push) Failing after 2m44s
check / check (push) Failing after 2m44s
Each app's sign-in, sign-up, password change and reset fields that had no exclusion now skip the 35 rules random passwords set off, on that app's paths and only when the query has no field of that name. Added gitea, Vaultwarden's admin token, WordPress pass1/pass2/post_password and its REST password, Drupal's forms and JSON, MediaWiki retype and password on the pages that take them, Mastodon's challenge, and Nextcloud's confirm, reset, change, share, LDAP renewal, encryption and OCS fields. Vaultwarden's clients send a hash, which needs no exclusion. Model: opus-5-5
This commit was merged in pull request #294.
This commit is contained in:
1 parent
8ab4280792
commit
db0912b335
15 files changed
+1043
-104
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