From c7c2123be9faa42c7b78ce008b8eee0de8a707ae Mon Sep 17 00:00:00 2001 From: clawbot Date: Wed, 11 Nov 2026 11:22:48 +0000 Subject: [PATCH] Core Rule Set: ls -la passes in q on a gitea user's or organisation's page (closes #349) On a path of one segment, q now skips 932250 too, unless the path ends in a server-side script's extension, such as .php or .cgi. The searches on a user's and an organisation's page are recorded from gitea 1.27.3 and Forgejo 17.0.1. Model: opus-5-5 --- SPEC.md | 18 ++++++-- internal/coreruleset/coreruleset_test.go | 42 +++++++++++++++++++ internal/coreruleset/gitea.conf | 11 ++++- .../coreruleset/testdata/forgejo-requests.txt | 4 ++ .../coreruleset/testdata/gitea-requests.txt | 11 +++++ 5 files changed, 80 insertions(+), 6 deletions(-) diff --git a/SPEC.md b/SPEC.md index 9225940..b6db52d 100644 --- a/SPEC.md +++ b/SPEC.md @@ -828,10 +828,20 @@ The changes hold in front of every app, and no setting undoes them: - branches and tags; - packages, on its pages and in its API. - A user's or an organisation's page is any path of one segment, so on - every such path, on any app, `q` skips 930120, 932120 and 932260, for - example on `/search`. It keeps 932250 there, so `ls -la` is refused on - those pages. + A user's or an organisation's page has a path of one segment, such as + `/alice`. So on any app, `q` skips these rules on every path of one + segment, such as `/search`. Such a path keeps 932250 when it ends in a + server-side script's extension, such as `/index.php`, so `ls -la` is + refused there. The extensions are these, in upper or lower case: + - `.php`; + - `.phtml`; + - `.php5`; + - `.cgi`; + - `.pl`; + - `.py`; + - `.asp`; + - `.aspx`; + - `.jsp`. Some of these searches also skip the rule for Java class names (944130), which refused `org.apache.commons.logging`. diff --git a/internal/coreruleset/coreruleset_test.go b/internal/coreruleset/coreruleset_test.go index eae74e7..b1dca64 100644 --- a/internal/coreruleset/coreruleset_test.go +++ b/internal/coreruleset/coreruleset_test.go @@ -1063,6 +1063,48 @@ func TestGiteaAttacksAreRefused(t *testing.T) { } } +func TestGiteaSearchOnAUsersOrAnOrganisationsPageLetsLsThrough(t *testing.T) { + t.Parallel() + + crs := atDefaults(t) + + // The searches of a user's and of an organisation's repositories. + for _, target := range []string{ + "/alice?tab=repositories&q=ls%20-la", + "/acme?q=ls%20-la&sort=recentupdate", + "/jane.doe?tab=repositories&q=ls%20-la", + } { + wantResult(t, crs, get(target), coreruleset.Result{}) + } + + // 932250 still applies on a path that ends in a server-side script's + // extension, to another field, and on a path of two segments. + for _, target := range []string{ + "/index.php?q=ls%20-la", + "/INDEX.PHP?q=ls%20-la", + "/index%2Ephp?q=ls%20-la", + "/search.cgi?q=ls%20-la", + "/alice?tab=ls%20-la", + "/alice/tools?q=ls%20-la", + } { + wantResult(t, crs, get(target), matched(932250)) + } + + // A command after a ";" in q is still refused on each of these paths. + const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default + + for _, target := range []string{ + "/alice?tab=repositories&q=", + "/index.php?q=", + "/wp-login.php?q=", + "/alice/tools?q=", + } { + if got := inspect(t, crs, get(target+commandInjection)); got.Score < threshold { + t.Errorf("%s%s let through: %+v", target, commandInjection, got) + } + } +} + // WordPress's paths: signing in, posting a comment, admin-ajax.php, and the // REST API routes of a post and of the tags. const ( diff --git a/internal/coreruleset/gitea.conf b/internal/coreruleset/gitea.conf index abd381c..8ca6568 100644 --- a/internal/coreruleset/gitea.conf +++ b/internal/coreruleset/gitea.conf @@ -135,8 +135,8 @@ SecRule REQUEST_FILENAME "@rx ^/(?:repo/search|\ ctl:ruleRemoveTargetById=932260;ARGS:q" # The repositories on a user's or an organisation's page are searched with q -# too. Their path has one segment, as other apps' pages such as /index.php do, -# so q keeps 932250 there and skips only these rules: +# too. Their path has one segment, so q skips these rules on every path of one +# segment, other apps' pages such as /index.php included: # 930120: .profile # 932120: powershell-scripts # 932260: python3-app @@ -146,6 +146,13 @@ SecRule REQUEST_FILENAME "@rx ^/[^/]+$" \ ctl:ruleRemoveTargetById=932120;ARGS:q,\ ctl:ruleRemoveTargetById=932260;ARGS:q" +# On those pages q skips 932250 too, which refused ls -la. A path keeps it when +# it ends in a server-side script's extension, such as /index.php or /search.cgi. +SecRule REQUEST_FILENAME "@rx ^/[^/]+$" \ + "id:900606,phase:1,pass,nolog,chain" + SecRule REQUEST_FILENAME "!@rx \.(?:php|phtml|php5|cgi|pl|py|asp|aspx|jsp)$" \ + "t:lowercase,ctl:ruleRemoveTargetById=932250;ARGS:q" + # These requests name a file in the path, which skips these rules there: # 920500: notes.txt~ # 934100: process.env.d.ts diff --git a/internal/coreruleset/testdata/forgejo-requests.txt b/internal/coreruleset/testdata/forgejo-requests.txt index cb36ae4..6a9d2d8 100644 --- a/internal/coreruleset/testdata/forgejo-requests.txt +++ b/internal/coreruleset/testdata/forgejo-requests.txt @@ -468,3 +468,7 @@ {"method": "GET", "target": "/repo-avatars/f9e66d43eb4888aac5d9d93745661631a09b4c73969226a3e89b2911ece215ba", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice", "Accept-Encoding: gzip, deflate", "Cookie: session=00a162ad65e95a87; lang=en-US"], "body": ""} {"method": "GET", "target": "/avatars/53bce4f1dfa0fe8e7ca126f91b35d3a6?size=64", "headers": ["Host: example.com", "Connection: keep-alive", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Accept: image/jxl,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", "Referer: http://example.com/alice", "Accept-Encoding: gzip, deflate", "Cookie: session=00a162ad65e95a87; lang=en-US"], "body": ""} {"method": "GET", "target": "/acme?q=python3&sort=recentupdate", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "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/acme", "Accept-Encoding: gzip, deflate", "Cookie: session=00a162ad65e95a87; lang=en-US"], "body": ""} +{"method": "GET", "target": "/alice?q=ls+-la&tab=repositories&sort=recentupdate", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "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?tab=repositories", "Accept-Encoding: gzip, deflate", "Cookie: session=4f05e01703d2563d; lang=en-US"], "body": ""} +{"method": "GET", "target": "/acme?q=ls+-la&sort=recentupdate", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "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/acme", "Accept-Encoding: gzip, deflate", "Cookie: session=4f05e01703d2563d; lang=en-US"], "body": ""} +{"method": "GET", "target": "/acme?q=powershell&sort=recentupdate", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "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/acme", "Accept-Encoding: gzip, deflate", "Cookie: session=4f05e01703d2563d; lang=en-US"], "body": ""} +{"method": "GET", "target": "/acme?q=.htaccess&sort=recentupdate", "headers": ["Host: example.com", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "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/acme", "Accept-Encoding: gzip, deflate", "Cookie: session=4f05e01703d2563d; lang=en-US"], "body": ""} diff --git a/internal/coreruleset/testdata/gitea-requests.txt b/internal/coreruleset/testdata/gitea-requests.txt index 65a6dc8..47ad283 100644 --- a/internal/coreruleset/testdata/gitea-requests.txt +++ b/internal/coreruleset/testdata/gitea-requests.txt @@ -91,6 +91,17 @@ /explore/organizations?q=docker-ci /api/v1/users/search?q=docker-ci +# Searches of the repositories on a user's and an organisation's page, recorded +# from gitea 1.27.3. +/alice?tab=repositories&q=ls+-la&sort=recentupdate +/alice?tab=repositories&q=python3&sort=recentupdate +/alice?tab=repositories&q=powershell&sort=recentupdate +/alice?tab=repositories&q=.htaccess&sort=recentupdate +/acme?q=ls+-la&sort=recentupdate +/acme?q=python3&sort=recentupdate +/acme?q=powershell&sort=recentupdate +/acme?q=.htaccess&sort=recentupdate + # Pages, raw files and edit pages of files, and the API's call for their # contents. /owner/repo/src/branch/main/package.json