Core Rule Set: a repository's name passes in the adopt form's id (closes #365)
check / check (push) Waiting to run

While bodies are read, the forms that adopt or delete an unadopted
repository were refused when the repository's name held a command, a
system file's name or a shell path, such as alice/python3-app,
alice/.profile or robin/assets. id there now skips 930120, 932120, 932160
and 932260. The paths are gitea's /-/admin/repos/unadopted, Forgejo's
/admin/repos/unadopted, and /user/settings/repos/unadopted, which both
serve.

Model: opus-5-5
This commit was merged in pull request #367.
This commit is contained in:
clawbot committed 2026-11-12 03:43:51 +01:00
1 parent 4b62bc84ce
commit 75f30a34b2
4 files changed
+108 -12

No files matched your search

+2 -2
View File
@@ -589,8 +589,8 @@ client's limits by tag. See "Request tags" in [`SPEC.md`](SPEC.md).
- Each supported app also has its own fixes, which let its ordinary requests - Each supported app also has its own fixes, which let its ordinary requests
through. Its recorded ordinary requests pass the Core Rule Set at the default through. Its recorded ordinary requests pass the Core Rule Set at the default
settings. The supported apps, by their names in `SWW_APPS`, are: settings. The supported apps, by their names in `SWW_APPS`, are:
- `gitea`, whose fixes let its container registry, searches, file names and - `gitea`, whose fixes let its container registry, searches, file names,
branch names through; branch names and repository names through;
- `forgejo`, a fork of gitea, which gets gitea's fixes too, and whose own - `forgejo`, a fork of gitea, which gets gitea's fixes too, and whose own
fixes let its searches through; fixes let its searches through;
- `wordpress`, whose fixes let through its searches, its names, its file - `wordpress`, whose fixes let through its searches, its names, its file
+12 -3
View File
@@ -826,9 +826,9 @@ one entry of `apps` in `internal/coreruleset/coreruleset.go`. gitea's fixes are:
with any script removed and returns only to its own pages. Every other cookie with any script removed and returns only to its own pages. Every other cookie
is read in full. is read in full.
3. On gitea's paths, some fields skip the rules that refused ordinary gitea 3. On gitea's paths, some fields skip the rules that refused ordinary gitea
requests there. They are the fields where gitea sends a search, a file name requests there. They are the fields where gitea sends a search, a file name,
or a branch name. `internal/coreruleset/gitea.conf` lists each rule and the a branch name or a repository's name. `internal/coreruleset/gitea.conf` lists
request it refused. The fields are: each rule and the request it refused. The fields are:
- `q` on gitea's searches of code, of issues and pull requests, and of - `q` on gitea's searches of code, of issues and pull requests, and of
commits. A code search holds code, such as `<script>`, commits. A code search holds code, such as `<script>`,
`SELECT * FROM users WHERE` or `../`. The searches of issues and pull `SELECT * FROM users WHERE` or `../`. The searches of issues and pull
@@ -924,6 +924,15 @@ one entry of `apps` in `internal/coreruleset/coreruleset.go`. gitea's fixes are:
- to start a release from a tag; - to start a release from a tag;
- to edit a branch protection rule; - to edit a branch protection rule;
- to list a directory's last commits. - to list a directory's last commits.
- `id` of the forms that adopt or delete an unadopted repository, for the
rules that refused repository names (930120, 932120, 932160, 932260). `id`
holds the repository's name, such as `python3-app` or `.profile`. The
admin forms put the owner in front. There the rule for shell paths refused
`robin/assets`, which holds `bin/as`. The forms post to these paths:
- `/-/admin/repos/unadopted`, gitea's admin form;
- `/admin/repos/unadopted`, Forgejo's admin form;
- `/user/settings/repos/unadopted`, the form in a user's settings, on
both.
- 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 - The passwords in a body, for each rule that random passwords set off. The
+23 -4
View File
@@ -1,8 +1,8 @@
# gitea's fixes: some fields skip the rules that refused ordinary gitea requests. # gitea's fixes: some fields skip the rules that refused ordinary gitea requests.
# They are the fields where gitea sends a search, a file name or a branch name. # They are the fields where gitea sends a search, a file name, a branch name or
# Each exclusion runs with the request, before the rules it switches off, those # a repository's name. Each exclusion runs with the request, before the rules it
# of phase 1 included, so this file is loaded before the Core Rule Set's rules. # switches off, those of phase 1 included, so this file is loaded before the
# Each comment names a request the rule refused. # Core Rule Set's rules. Each comment names a request the rule refused.
# Every rule below is skipped unless Inspect has set tx.app_gitea. It sets it # Every rule below is skipped unless Inspect has set tx.app_gitea. It sets it
# while SWW_APPS is unset, or lists gitea or forgejo for the request's hostname. # while SWW_APPS is unset, or lists gitea or forgejo for the request's hostname.
@@ -399,6 +399,25 @@ SecRule REQUEST_FILENAME \
"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 forms that adopt or delete an unadopted repository send its name in id.
# The admin forms put its owner in front, as alice/python3-app. id skips these
# rules there:
# 930120: alice/.profile
# 932120: alice/powershell-scripts
# 932160: robin/assets, which holds bin/as
# 932260: alice/python3-app
# The forms post to these paths:
# /-/admin/repos/unadopted: gitea's admin form
# /admin/repos/unadopted: Forgejo's admin form
# /user/settings/repos/unadopted: the form in a user's settings, on both
SecRule REQUEST_FILENAME \
"@rx ^/(?:(?:-/)?admin|user/settings)/repos/unadopted$" \
"id:900645,phase:1,pass,nolog,\
ctl:ruleRemoveTargetById=930120;ARGS:id,\
ctl:ruleRemoveTargetById=932120;ARGS:id,\
ctl:ruleRemoveTargetById=932160;ARGS:id,\
ctl:ruleRemoveTargetById=932260;ARGS:id"
# The passwords skip each rule that random passwords set off: # The passwords skip each rule that random passwords set off:
# password: signing in, signing up, linking an account, activating an account, # password: signing in, signing up, linking an account, activating an account,
# resetting or changing the password, deleting an account, and creating or # resetting or changing the password, deleting an account, and creating or
+71 -3
View File
@@ -8,6 +8,13 @@ import (
"sneak.berlin/go/smallwebwaf/internal/coreruleset" "sneak.berlin/go/smallwebwaf/internal/coreruleset"
) )
// giteaAdminUnadoptedRepos and forgejoAdminUnadoptedRepos are the admin pages
// of unadopted repositories. The forms that adopt or delete one post there.
const (
giteaAdminUnadoptedRepos = "/-/admin/repos/unadopted"
forgejoAdminUnadoptedRepos = "/admin/repos/unadopted"
)
// giteaSearches are searches of the pages gitea and Forgejo share whose q // giteaSearches are searches of the pages gitea and Forgejo share whose q
// skips the lists of system files and command names, in the eighth change. // skips the lists of system files and command names, in the eighth change.
// gitea serves its admin pages under /-/admin/ and Forgejo under /admin/. // gitea serves its admin pages under /-/admin/ and Forgejo under /admin/.
@@ -30,12 +37,12 @@ var giteaSearches = []string{
"/-/admin/users", "/-/admin/users",
"/-/admin/orgs", "/-/admin/orgs",
"/-/admin/repos", "/-/admin/repos",
"/-/admin/repos/unadopted", giteaAdminUnadoptedRepos,
"/-/admin/emails", "/-/admin/emails",
"/-/admin/emails/activate", "/-/admin/emails/activate",
"/admin/actions/runners", "/admin/actions/runners",
"/admin/users", "/admin/users",
"/admin/repos/unadopted", forgejoAdminUnadoptedRepos,
"/admin/emails/activate", "/admin/emails/activate",
} }
@@ -60,7 +67,7 @@ func TestGiteaAdminFormsLetHardValuesThroughInTheBody(t *testing.T) {
for _, value := range hardValues { for _, value := range hardValues {
adopt := "id=alice%2Flegacy-site&action=adopt&q=" + value + "&page=1" adopt := "id=alice%2Flegacy-site&action=adopt&q=" + value + "&page=1"
for _, path := range []string{"/-/admin/repos/unadopted", "/admin/repos/unadopted"} { for _, path := range []string{giteaAdminUnadoptedRepos, forgejoAdminUnadoptedRepos} {
r := request{http.MethodPost, path, []string{formData}} r := request{http.MethodPost, path, []string{formData}}
wantBody(t, crs, r, adopt, coreruleset.Result{}, adopt) wantBody(t, crs, r, adopt, coreruleset.Result{}, adopt)
} }
@@ -75,6 +82,67 @@ func TestGiteaAdminFormsLetHardValuesThroughInTheBody(t *testing.T) {
} }
} }
func TestGiteaFormsThatAdoptARepositoryLetItsNameThrough(t *testing.T) {
t.Parallel()
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
crs := readingBodies(t)
// The admin forms send the repository's owner and name in id, and the
// form in a user's settings only its name. robin/assets holds bin/as, a
// shell path.
ownerAndName := []string{
"alice%2Fpython3-app",
"alice%2Fpowershell-scripts",
"alice%2F.profile",
"robin%2Fassets",
}
for _, form := range []struct {
path string
ids []string
}{
{giteaAdminUnadoptedRepos, ownerAndName},
{forgejoAdminUnadoptedRepos, ownerAndName},
{
"/user/settings/repos/unadopted", // gitea's and Forgejo's
[]string{"python3-app", "powershell-scripts", ".profile"},
},
} {
r := request{http.MethodPost, form.path, []string{formData}}
for _, action := range []string{"adopt", "delete"} {
for _, id := range form.ids {
body := "id=" + id + "&action=" + action
wantBody(t, crs, r, body, coreruleset.Result{}, body)
}
}
// Another field keeps those rules.
body := "id=" + form.ids[0] + "&action=adopt&page=python3"
wantBody(t, crs, r, body, matched(932260), body)
// Attacks in id are still refused.
for _, attack := range []string{
sqlInjection,
scriptInjection,
commandInjection,
"..%2F..%2Fetc%2Fpasswd",
} {
body := "id=" + attack + "&action=adopt"
if got, _ := inspectBody(t, crs, r, body); got.Score < threshold {
t.Errorf("POST %s %s let through: %+v", form.path, body, got)
}
}
}
// On another path, id keeps those rules.
r := request{http.MethodPost, "/-/admin/repos", []string{formData}}
body := "id=alice%2Fpython3-app&action=adopt&q=&page=1"
wantBody(t, crs, r, body, matched(932260), body)
}
func TestGiteaAttacksAreRefusedInItsSearches(t *testing.T) { func TestGiteaAttacksAreRefusedInItsSearches(t *testing.T) {
t.Parallel() t.Parallel()