From 9dc913ecb51a41fcc185d4fe00cc5cc95ea6dd08 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 made of the characters a user's or an organisation's name may hold, 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 | 21 +++++++-- internal/coreruleset/coreruleset_test.go | 44 +++++++++++++++++++ internal/coreruleset/gitea.conf | 14 +++++- .../coreruleset/testdata/forgejo-requests.txt | 4 ++ .../coreruleset/testdata/gitea-requests.txt | 11 +++++ 5 files changed, 88 insertions(+), 6 deletions(-) diff --git a/SPEC.md b/SPEC.md index 9225940..e66a966 100644 --- a/SPEC.md +++ b/SPEC.md @@ -828,10 +828,23 @@ 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`. But it skips 932250 only on a path made of + the characters a name may hold: letters, digits, `-`, `_` and dots, the + first a letter or digit. So `ls -la` is refused on + `/login.jsp;jsessionid=abc`. `q` also keeps 932250 on a path that ends + in a server-side script's extension, such as `/index.php`. 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..dee8ab8 100644 --- a/internal/coreruleset/coreruleset_test.go +++ b/internal/coreruleset/coreruleset_test.go @@ -1063,6 +1063,50 @@ 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 or holds a character no name holds, 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", + "/login.jsp;jsessionid=abc?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..afc8778 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,16 @@ 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, but only on a path +# made of the characters a user's or an organisation's name may hold. Those are +# letters, digits, -, _ and dots, the first a letter or digit, so +# /login.jsp;jsessionid=abc keeps 932250. A path that ends in a server-side +# script's extension keeps it too, such as /index.php or /search.cgi. +SecRule REQUEST_FILENAME "@rx ^/[0-9A-Za-z][-.\w]*$" \ + "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