Core Rule Set: a repository's name passes in the adopt form's id (closes #365) #367

Merged
clawbot merged 1 commits from issue-365-adopt-form-repository-names into next 2026-11-12 03:43:52 +01:00
4 changed files with 108 additions and 12 deletions

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
through. Its recorded ordinary requests pass the Core Rule Set at the default
settings. The supported apps, by their names in `SWW_APPS`, are:
- `gitea`, whose fixes let its container registry, searches, file names and
branch names through;
- `gitea`, whose fixes let its container registry, searches, file names,
branch names and repository names through;
- `forgejo`, a fork of gitea, which gets gitea's fixes too, and whose own
fixes let its searches through;
- `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
is read in full.
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
or a branch name. `internal/coreruleset/gitea.conf` lists each rule and the
request it refused. The fields are:
requests there. They are the fields where gitea sends a search, a file name,
a branch name or a repository's name. `internal/coreruleset/gitea.conf` lists
each rule and the request it refused. The fields are:
- `q` on gitea's searches of code, of issues and pull requests, and of
commits. A code search holds code, such as `<script>`,
`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 edit a branch protection rule;
- 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
`actions/upload-artifact@v3` (920450).
- 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.
# They are the fields where gitea sends a search, a file name or a branch name.
# Each exclusion runs with the request, before the rules it switches off, those
# of phase 1 included, so this file is loaded before the Core Rule Set's rules.
# Each comment names a request the rule refused.
# They are the fields where gitea sends a search, a file name, a branch name or
# a repository's name. Each exclusion runs with the request, before the rules it
# switches off, those of phase 1 included, so this file is loaded before the
# 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
# 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,\
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:
# password: signing in, signing up, linking an account, activating an account,
# 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"
)
// 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
// skips the lists of system files and command names, in the eighth change.
// gitea serves its admin pages under /-/admin/ and Forgejo under /admin/.
@@ -30,12 +37,12 @@ var giteaSearches = []string{
"/-/admin/users",
"/-/admin/orgs",
"/-/admin/repos",
"/-/admin/repos/unadopted",
giteaAdminUnadoptedRepos,
"/-/admin/emails",
"/-/admin/emails/activate",
"/admin/actions/runners",
"/admin/users",
"/admin/repos/unadopted",
forgejoAdminUnadoptedRepos,
"/admin/emails/activate",
}
@@ -60,7 +67,7 @@ func TestGiteaAdminFormsLetHardValuesThroughInTheBody(t *testing.T) {
for _, value := range hardValues {
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}}
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) {
t.Parallel()