1 Commits
Author SHA1 Message Date
clawbot 4dbd134d49 Core Rule Set: random passwords pass at sign-in (closes #276)
check / check (push) Successful in 6m17s
A password manager's random password set off 35 Core Rule Set rules,
about 1 in 40 refused. The password fields the app exclusions name now
skip those rules on their paths: WordPress pwd, Drupal pass, MediaWiki
wpPassword, lgpassword and password, Mastodon's sign-in, sign-up,
password change, move and delete passwords, and Nextcloud's sign-in
password. The tests draw 2,000 random passwords from a fixed seed, plus
one rare password for each rule the 2,000 do not set off. Those that set
off a rule in a field no exclusion names are sent to every password
field. An SQL injection or a path traversal sent as a password now
passes; a script injection is still refused.

Model: opus-5-5
2026-11-10 09:31:51 +00:00
7 changed files with 61 additions and 100 deletions

No files matched your search

+1 -1
View File
@@ -602,7 +602,7 @@ client's limits by tag. See "Request tags" in [`SPEC.md`](SPEC.md).
- Nextcloud: its sync client gets through. On Nextcloud's own paths, its
WebDAV methods, searches, file names, account names, comments and
documents do too.
- A random password, as a password manager makes one, gets through these forms:
- A password made of any printable characters gets through these forms:
- signing in, on WordPress, Drupal, MediaWiki and Nextcloud;
- signing in, signing up, changing the password, and moving or deleting the
account, on Mastodon.
+34 -57
View File
@@ -920,19 +920,13 @@ The changes hold in front of every app, and no setting undoes them:
- `newcontent` of a plugin's or a theme's file saved from its editor,
which is PHP.
- `pwd`, the password when signing in, for each rule that random
passwords set off:
920270, 920540, 921200, 930100, 930110, 930120, 932125, 932130,
932160, 932230, 932250, 932260, 932270, 932280, 932330, 932340,
932370, 932380, 933100, 933135, 933180, 933210, 941100, 941120,
941160, 941180, 942100, 942151, 942190, 942230, 942290, 942320,
942540, 942550, 944150.
`' OR '1'='1`, `../../etc/passwd`, `; cat /etc/passwd` and
`<img src=x onerror=x()>` sent as the password reach WordPress;
`<script>alert(1)</script>` and `javascript:alert(1)` are still
refused.
passwords set off (920270, 920540, 921200, 930100, 930110, 930120,
932125, 932130, 932160, 932230, 932250, 932260, 932270, 932280,
932330, 932340, 932370, 932380, 933100, 933135, 933180, 933210,
941100, 941120, 941160, 941180, 942100, 942151, 942190, 942230,
942290, 942320, 942540, 942550, 944150). An SQL injection or a path
traversal sent as the password reaches WordPress. A script injection
is still refused.
- `title`, `excerpt` and `name` in a JSON body sent to any REST API
route under `/wp/v2/`, for the rules on system files and commands
(930120, 932120, 932250, 932260).
@@ -1001,17 +995,12 @@ The changes hold in front of every app, and no setting undoes them:
JSON:API, which hold HTML and any code it shows.
- `name` and `pass` when signing in (930120, 932120, 932250, 932260).
- `pass`, the password when signing in, in form data or JSON, for each rule
that random passwords set off:
920270, 920540, 921200, 930100, 930110, 930120, 932125, 932130, 932160,
932230, 932250, 932260, 932270, 932280, 932330, 932340, 932370, 932380,
933100, 933135, 933180, 933210, 941100, 941120, 941160, 941180, 942100,
942151, 942190, 942230, 942290, 942320, 942540, 942550, 944150.
`' OR '1'='1`, `../../etc/passwd`, `; cat /etc/passwd` and
`<img src=x onerror=x()>` sent as the password reach Drupal;
`<script>alert(1)</script>` and `javascript:alert(1)` are still refused.
that random passwords set off (920270, 920540, 921200, 930100, 930110,
930120, 932125, 932130, 932160, 932230, 932250, 932260, 932270, 932280,
932330, 932340, 932370, 932380, 933100, 933135, 933180, 933210, 941100,
941120, 941160, 941180, 942100, 942151, 942190, 942230, 942290, 942320,
942540, 942550, 944150). An SQL injection or a path traversal sent as the
password reaches Drupal. A script injection is still refused.
- `name` when resetting a password through Drupal's JSON format on
`/user/password`, for the same four rules.
- `mail` in that same reset, for 932120 and 932260 only.
@@ -1091,20 +1080,16 @@ The changes hold in front of every app, and no setting undoes them:
- `wpTextbox1` on MediaWiki's pages;
- `text`, `appendtext` and `prependtext` on `/api.php`;
- `source` in the JSON body of `/rest.php/`.
- The passwords when signing in:
- The passwords when signing in, for each rule that random passwords set off
(920270, 920540, 921200, 930100, 930110, 930120, 932125, 932130, 932160,
932230, 932250, 932260, 932270, 932280, 932330, 932340, 932370, 932380,
933100, 933135, 933180, 933210, 941100, 941120, 941160, 941180, 942100,
942151, 942190, 942230, 942290, 942320, 942540, 942550, 944150):
- `wpPassword` on MediaWiki's pages;
- `lgpassword` and `password` on `/api.php`.
They skip each rule that random passwords set off:
920270, 920540, 921200, 930100, 930110, 930120, 932125, 932130, 932160,
932230, 932250, 932260, 932270, 932280, 932330, 932340, 932370, 932380,
933100, 933135, 933180, 933210, 941100, 941120, 941160, 941180, 942100,
942151, 942190, 942230, 942290, 942320, 942540, 942550, 944150.
`' OR '1'='1`, `../../etc/passwd`, `; cat /etc/passwd` and
`<img src=x onerror=x()>` sent as a password reach MediaWiki;
`<script>alert(1)</script>` and `javascript:alert(1)` are still refused.
An SQL injection or a path traversal sent as a password reaches
MediaWiki. A script injection is still refused.
These body exclusions matter only while `SWW_WAF_BODY_LIMIT` is set. None
applies when the query has a parameter of the same name.
@@ -1231,7 +1216,11 @@ The changes hold in front of every app, and no setting undoes them:
Each name contains `.forward`, a file in a home folder. So 930120 refused
every report the web app sent. The keys' values keep every rule.
The passwords in a body:
The passwords in a body skip each rule that random passwords set off
(920270, 920540, 921200, 930100, 930110, 930120, 932125, 932130, 932160,
932230, 932250, 932260, 932270, 932280, 932330, 932340, 932370, 932380,
933100, 933135, 933180, 933210, 941100, 941120, 941160, 941180, 942100,
942151, 942190, 942230, 942290, 942320, 942540, 942550, 944150):
- on `/auth`, `/settings/migration` and `/settings/delete`, and the paths
under them:
- `user[password]`, `user[password_confirmation]` and
@@ -1241,16 +1230,8 @@ The changes hold in front of every app, and no setting undoes them:
- `form_delete_confirmation[password]`;
- `password` on `/api/v1/accounts`, in form data or JSON.
They skip each rule that random passwords set off:
920270, 920540, 921200, 930100, 930110, 930120, 932125, 932130, 932160,
932230, 932250, 932260, 932270, 932280, 932330, 932340, 932370, 932380,
933100, 933135, 933180, 933210, 941100, 941120, 941160, 941180, 942100,
942151, 942190, 942230, 942290, 942320, 942540, 942550, 944150.
`' OR '1'='1`, `../../etc/passwd`, `; cat /etc/passwd` and
`<img src=x onerror=x()>` sent as a password reach Mastodon;
`<script>alert(1)</script>` and `javascript:alert(1)` are still refused.
An SQL injection or a path traversal sent as a password reaches Mastodon. A
script injection is still refused.
These body exclusions matter only while `SWW_WAF_BODY_LIMIT` is set. None
applies when the query has a parameter of the same name.
@@ -1385,17 +1366,13 @@ The changes hold in front of every app, and no setting undoes them:
- `name` on `/remote.php/dav/systemtags/`, a new tag.
`password` in the body of `/login`, the password when signing in, skips each
rule that random passwords set off:
920270, 920540, 921200, 930100, 930110, 930120, 932125, 932130, 932160,
932230, 932250, 932260, 932270, 932280, 932330, 932340, 932370, 932380,
933100, 933135, 933180, 933210, 941100, 941120, 941160, 941180, 942100,
942151, 942190, 942230, 942290, 942320, 942540, 942550, 944150.
It applies only when the query has no `password` parameter. `' OR '1'='1`,
`../../etc/passwd`, `; cat /etc/passwd` and `<img src=x onerror=x()>` sent
as the password reach Nextcloud; `<script>alert(1)</script>` and
`javascript:alert(1)` are still refused.
rule that random passwords set off (920270, 920540, 921200, 930100, 930110,
930120, 932125, 932130, 932160, 932230, 932250, 932260, 932270, 932280,
932330, 932340, 932370, 932380, 933100, 933135, 933180, 933210, 941100,
941120, 941160, 941180, 942100, 942151, 942190, 942230, 942290, 942320,
942540, 942550, 944150). It applies only when the query has no `password`
parameter. An SQL injection or a path traversal sent as the password reaches
Nextcloud. A script injection is still refused.
Some fields hold sentences a person writes:
- `note`, a share's note, on `apps/files_sharing/api/v1/shares` of the OCS
+9 -30
View File
@@ -846,35 +846,16 @@ func wantRandomPasswordsPass(t *testing.T, r request, name string) {
}
}
// wantPasswordAttacksPass checks that an SQL injection, a path traversal, a
// 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.
func wantPasswordAttacksPass(t *testing.T, r request, name string) {
// wantPasswordAttacksRefused checks that each of attacks, sent as the whole
// password in the field name of r's body, is refused while bodies are read.
func wantPasswordAttacksRefused(
t *testing.T, r request, name string, attacks ...string,
) {
t.Helper()
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
for _, attack := range []string{
"' OR '1'='1", pathTraversal, "; cat /etc/passwd",
"<img src=x onerror=x()>",
} {
body := passwordBody(t, r, name, attack)
if got, _ := inspectBody(t, readingBodies(t), r, body); got.Score >= threshold {
t.Errorf("%s %s %s %q refused: %+v", r.method, r.target, name,
attack, got)
}
}
}
// wantPasswordAttacksRefused checks that a script tag and a javascript: link,
// each sent as the whole password in the field name of r's body, are refused
// while bodies are read.
func wantPasswordAttacksRefused(t *testing.T, r request, name string) {
t.Helper()
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
for _, attack := range []string{scriptTag, "javascript:alert(1)"} {
for _, attack := range attacks {
body := passwordBody(t, r, name, attack)
if got, _ := inspectBody(t, readingBodies(t), r, body); got.Score < threshold {
t.Errorf("%s %s %s %q let through: %+v", r.method, r.target, name,
@@ -1613,8 +1594,9 @@ func TestWordPressPasswordsPass(t *testing.T) {
signIn := request{http.MethodPost, wpLogin, []string{formData}}
wantRandomPasswordsPass(t, signIn, "pwd")
wantPasswordAttacksPass(t, signIn, "pwd")
wantPasswordAttacksRefused(t, signIn, "pwd")
// No rule pwd keeps catches the SQL injection or the path traversal.
wantPasswordAttacksRefused(t, signIn, "pwd", scriptTag)
}
func TestWordPressExclusionsAreNarrow(t *testing.T) {
@@ -1674,9 +1656,6 @@ const injection = "1' OR '1'='1"
// scriptTag is a script injection, not escaped.
const scriptTag = "<script>alert(1)</script>"
// pathTraversal is a path traversal, not escaped.
const pathTraversal = "../../etc/passwd"
// readingBodies returns the Core Rule Set as sww runs it by
// default, but reading bodies up to bodyLimit.
func readingBodies(t *testing.T) *coreruleset.CoreRuleSet {
+4 -2
View File
@@ -182,8 +182,10 @@ func TestDrupalPasswordPasses(t *testing.T) {
t.Parallel()
wantRandomPasswordsPass(t, r, "pass")
wantPasswordAttacksPass(t, r, "pass")
wantPasswordAttacksRefused(t, r, "pass")
// No rule pass keeps catches the SQL injection or the path
// traversal.
wantPasswordAttacksRefused(t, r, "pass", scriptTag)
})
}
}
+5 -5
View File
@@ -152,7 +152,6 @@ var mastodonFormFields = []mastodonBodyField{
//nolint:gochecknoglobals // a constant cannot be a list
var mastodonPasswordForms = []mastodonBodyField{
{http.MethodPost, mastodonSignIn, mastodonPassword},
{http.MethodPost, mastodonAccount, mastodonPassword},
{http.MethodPost, mastodonAccount, "user[password_confirmation]="},
{http.MethodPost, mastodonAccount, "user[current_password]="},
{http.MethodPost, "/settings/migration", "account_migration[current_password]="},
@@ -314,8 +313,10 @@ func TestMastodonPasswordsPass(t *testing.T) {
name := strings.TrimSuffix(sent.field, "=")
wantRandomPasswordsPass(t, r, name)
wantPasswordAttacksPass(t, r, name)
wantPasswordAttacksRefused(t, r, name)
// No rule these fields keep catches the SQL injection or the path
// traversal.
wantPasswordAttacksRefused(t, r, name, scriptTag)
})
}
@@ -326,8 +327,7 @@ func TestMastodonPasswordsPass(t *testing.T) {
[]string{jsonBody}}
wantRandomPasswordsPass(t, r, "password")
wantPasswordAttacksPass(t, r, "password")
wantPasswordAttacksRefused(t, r, "password")
wantPasswordAttacksRefused(t, r, "password", scriptTag)
})
}
+4 -3
View File
@@ -174,7 +174,6 @@ var mediawikiBodyFields = []mediawikiBodyField{
//nolint:gochecknoglobals // a constant cannot be a list
var mediawikiPasswordFields = []mediawikiBodyField{
{mediawikiSignIn, "wpPassword="},
{"/w" + mediawikiSignInPage, "wpPassword="},
{mediawikiAPI, "lgpassword="},
{mediawikiAPI, passwordField},
}
@@ -253,8 +252,10 @@ func TestMediaWikiPasswordsPass(t *testing.T) {
name := strings.TrimSuffix(sent.field, "=")
wantRandomPasswordsPass(t, r, name)
wantPasswordAttacksPass(t, r, name)
wantPasswordAttacksRefused(t, r, name)
// No rule these fields keep catches the SQL injection or the path
// traversal.
wantPasswordAttacksRefused(t, r, name, scriptTag)
})
}
}
+4 -2
View File
@@ -288,8 +288,10 @@ func TestNextcloudPasswordPasses(t *testing.T) {
r := request{http.MethodPost, nextcloudSignIn, []string{formData}}
wantRandomPasswordsPass(t, r, "password")
wantPasswordAttacksPass(t, r, "password")
wantPasswordAttacksRefused(t, r, "password")
// No rule the password keeps catches the SQL injection or the path
// traversal.
wantPasswordAttacksRefused(t, r, "password", scriptTag)
}
func TestNextcloudSentencesPassInTheTexts(t *testing.T) {