Core Rule Set: the package registries' searches of gitea and Forgejo pass (closes #354)
check / check (push) Waiting to run
check / check (push) Waiting to run
Recorded from gitea 1.27.3 and Forgejo 17.0.1 with each registry's own client, and curl for the requests no client sends, with python3, powershell, ls -la, .htaccess, file names and plain words. q on the Cargo, Chef, Composer and Conan searches and NuGet's search service joins 900603. 900605 lets /bin/sh through on Composer and Conan. New: 900609 for npm's text; 900611, in phase 2, lets one q shaped as a Conan reference with no +, */ or --, such as zlib/*, skip 942100 if 942100 passes it with each /* comment read as a space; 900612 for NuGet's Search(), Packages() and their counts: searchTerm, $filter, and the names $filter, $orderby and $top for 942290. Model: opus-5-5
This commit is contained in:
1 parent
b84a33fb57
commit
055876c7ed
5 files changed
+476
-21
No files matched your search
@@ -271,15 +271,28 @@ const giteaAPIPackageSearch = "/api/v1/packages/alice?q="
|
||||
func TestGiteaAttacksAreRefusedInItsAPISearches(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const threshold = 5 // SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default
|
||||
|
||||
defaults, bodies := atDefaults(t), readingBodies(t)
|
||||
searches := slices.Concat(
|
||||
checkSearchesRefuseAttacks(
|
||||
t,
|
||||
giteaAPISearches,
|
||||
[]string{giteaAPIPackageSearch},
|
||||
)
|
||||
}
|
||||
|
||||
for _, search := range searches {
|
||||
// checkSearchesRefuseAttacks checks that SQL injection, a path traversal,
|
||||
// script injection and a shell command stay refused in the field of each
|
||||
// search, in the query string and in a form data body. Each search ends in
|
||||
// its field. /bin/sh stays refused too in the field of each of
|
||||
// refusingBinSh, but not of lettingBinSh.
|
||||
func checkSearchesRefuseAttacks(
|
||||
t *testing.T, refusingBinSh, lettingBinSh []string,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
const threshold = 5 // SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default
|
||||
|
||||
defaults, bodies := atDefaults(t), readingBodies(t)
|
||||
|
||||
for _, search := range slices.Concat(refusingBinSh, lettingBinSh) {
|
||||
path, field, _ := strings.Cut(search, "?")
|
||||
|
||||
for _, attack := range []string{
|
||||
@@ -303,8 +316,7 @@ func TestGiteaAttacksAreRefusedInItsAPISearches(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// /bin/sh stays refused on the searches that are not of packages.
|
||||
for _, search := range giteaAPISearches {
|
||||
for _, search := range refusingBinSh {
|
||||
if got := inspect(t, defaults, get(search+binSh)); got.Score < threshold {
|
||||
t.Errorf("%s%s let through: %+v", search, binSh, got)
|
||||
}
|
||||
@@ -314,14 +326,9 @@ func TestGiteaAttacksAreRefusedInItsAPISearches(t *testing.T) {
|
||||
func TestGiteaAPISearchExclusionsAreNarrow(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const threshold = 5 // SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default
|
||||
|
||||
crs := atDefaults(t)
|
||||
|
||||
// /bin/sh, a shell command after a ";", a path traversal and the hard
|
||||
// values stay refused in each search's field on paths near the
|
||||
// searches, and in another field on them.
|
||||
for _, target := range []string{
|
||||
// Each search's field on paths near the searches, and another field on
|
||||
// them.
|
||||
checkValuesStayRefused(t, []string{
|
||||
"/api/v1/topics/search/x?q=",
|
||||
"/api/v1/orgs/acme/teams?q=",
|
||||
"/api/v1/orgs/acme/teams/search/x?q=",
|
||||
@@ -337,7 +344,19 @@ func TestGiteaAPISearchExclusionsAreNarrow(t *testing.T) {
|
||||
"/api/v1/admin/unadopted?q=",
|
||||
"/api/v1/repos/alice/tools/milestones?q=",
|
||||
"/api/v1/packages/alice?other=",
|
||||
} {
|
||||
})
|
||||
}
|
||||
|
||||
// checkValuesStayRefused checks that /bin/sh, a shell command after a ";", a
|
||||
// path traversal and the hard values stay refused at the end of each target.
|
||||
func checkValuesStayRefused(t *testing.T, targets []string) {
|
||||
t.Helper()
|
||||
|
||||
const threshold = 5 // SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default
|
||||
|
||||
crs := atDefaults(t)
|
||||
|
||||
for _, target := range targets {
|
||||
for _, value := range slices.Concat(
|
||||
[]string{binSh, commandInjection, pathTraversal},
|
||||
hardValues,
|
||||
@@ -348,3 +367,135 @@ func TestGiteaAPISearchExclusionsAreNarrow(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// giteaRegistrySearches are the package registries' searches whose field
|
||||
// skips the lists of system files and command names. Each ends in the field
|
||||
// it sends its words in.
|
||||
//
|
||||
//nolint:gochecknoglobals // a constant cannot be a list
|
||||
var giteaRegistrySearches = []string{
|
||||
"/api/packages/alice/cargo/api/v1/crates?q=",
|
||||
"/api/packages/alice/chef/api/v1/search?q=",
|
||||
"/api/packages/alice/chef/api/v1/cookbooks?q=",
|
||||
"/api/packages/alice/nuget/query?q=",
|
||||
"/api/packages/alice/nuget/Search()?searchTerm=",
|
||||
"/api/packages/alice/nuget/Packages()?$filter=",
|
||||
"/api/packages/alice/nuget/Search()/$count?searchTerm=",
|
||||
"/api/packages/alice/nuget/Packages()/$count?$filter=",
|
||||
}
|
||||
|
||||
// giteaRegistrySearchesLettingBinSh are the package registries' searches
|
||||
// whose field also lets /bin/sh through, since a Composer, Conan or npm name
|
||||
// can hold a slash.
|
||||
//
|
||||
//nolint:gochecknoglobals // a constant cannot be a list
|
||||
var giteaRegistrySearchesLettingBinSh = []string{
|
||||
"/api/packages/alice/composer/search.json?q=",
|
||||
"/api/packages/alice/conan/v1/conans/search?q=",
|
||||
"/api/packages/alice/conan/v2/conans/search?q=",
|
||||
"/api/packages/alice/npm/-/v1/search?text=",
|
||||
}
|
||||
|
||||
func TestGiteaAttacksAreRefusedInItsRegistrySearches(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
checkSearchesRefuseAttacks(
|
||||
t,
|
||||
giteaRegistrySearches,
|
||||
giteaRegistrySearchesLettingBinSh,
|
||||
)
|
||||
}
|
||||
|
||||
func TestGiteaRegistrySearchExclusionsAreNarrow(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Each search's field on paths near the searches, and another field on
|
||||
// them.
|
||||
checkValuesStayRefused(t, []string{
|
||||
"/api/packages/alice/cargo/api/v1/crates/tools-cli?q=",
|
||||
"/api/packages/alice/chef/api/v1/cookbooks/tools?q=",
|
||||
"/api/packages/alice/composer/packages.json?q=",
|
||||
"/api/packages/alice/conan/v1/conans/tools/1.0/alice/stable/search?q=",
|
||||
"/api/packages/alice/nuget/query/x?q=",
|
||||
"/api/packages/alice/nuget/FindPackagesById()?id=",
|
||||
"/api/packages/alice/nuget/FindPackagesById()/$count?id=",
|
||||
"/api/packages/alice/nuget/Search()/$count/x?searchTerm=",
|
||||
"/api/packages/alice/npm/tools-cli?text=",
|
||||
"/api/packages/alice/cargo/api/v1/crates?other=",
|
||||
"/api/packages/alice/chef/api/v1/search?other=",
|
||||
"/api/packages/alice/composer/search.json?type=",
|
||||
"/api/packages/alice/conan/v2/conans/search?other=",
|
||||
"/api/packages/alice/nuget/query?other=",
|
||||
"/api/packages/alice/nuget/Search()?targetFramework=",
|
||||
"/api/packages/alice/nuget/Packages()?$orderby=",
|
||||
"/api/packages/alice/nuget/Search()/$count?targetFramework=",
|
||||
"/api/packages/alice/nuget/Packages()/$count?$orderby=",
|
||||
"/api/packages/alice/npm/-/v1/search?q=",
|
||||
})
|
||||
|
||||
const threshold = 5 // SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default
|
||||
|
||||
crs := atDefaults(t)
|
||||
|
||||
// The rule for MongoDB operators (942290) still refuses the name of
|
||||
// another operator on NuGet's searches, and $filter on NuGet's other
|
||||
// paths. The rule for SQL injection (942100) still refuses tools/* in a
|
||||
// field other than a Conan search's, SQL injection in a second q after a
|
||||
// Conan pattern, SQL comments or dots that stand in for spaces, and SQL
|
||||
// injection holding + or --.
|
||||
for _, target := range []string{
|
||||
"/api/packages/alice/nuget/Search()?$where=1",
|
||||
"/api/packages/alice/nuget/Packages()?$ne=1",
|
||||
"/api/packages/alice/nuget/Search()/$count?$where=1",
|
||||
"/api/packages/alice/nuget/FindPackagesById()?$filter=IsLatestVersion",
|
||||
"/api/packages/alice/nuget/FindPackagesById()/$count?$filter=IsLatestVersion",
|
||||
"/api/packages/alice/composer/search.json?q=tools%2F%2A",
|
||||
"/api/packages/alice/conan/v2/conans/search?other=tools%2F%2A",
|
||||
"/api/packages/alice/conan/v2/conans/search?q=tools%2F%2A&q=" +
|
||||
sqlInjection,
|
||||
"/api/packages/alice/conan/v2/conans/search?q=1%2F%2A%2A%2For%2F%2A%2A%2F1",
|
||||
"/api/packages/alice/conan/v2/conans/search?q=1%2F%2A%40%2A%2For-1",
|
||||
"/api/packages/alice/conan/v2/conans/search?q=1%2Bor%2B1--",
|
||||
"/api/packages/alice/conan/v2/conans/search?q=1-or-1--",
|
||||
"/api/packages/alice/conan/v2/conans/search?q=x%2F1.or.1",
|
||||
"/api/packages/alice/conan/v2/conans/search?q=x%2F1.and.1",
|
||||
"/api/packages/alice/conan/v2/conans/search?q=1.having.1",
|
||||
} {
|
||||
if got := inspect(t, crs, get(target)); got.Score < threshold {
|
||||
t.Errorf("%s let through: %+v", target, got)
|
||||
}
|
||||
}
|
||||
|
||||
// SQL injection in a q in the body is refused, even after a Conan
|
||||
// pattern in the query string.
|
||||
search := request{
|
||||
http.MethodPost,
|
||||
"/api/packages/alice/conan/v2/conans/search?q=zlib",
|
||||
[]string{formData},
|
||||
}
|
||||
|
||||
body := "q=" + sqlInjection
|
||||
if got, _ := inspectBody(t, readingBodies(t), search, body); got.Score < threshold {
|
||||
t.Errorf("POST %s %s let through: %+v", search.target, body, got)
|
||||
}
|
||||
|
||||
// So is SQL injection with dots for spaces, in a q sent only in the body.
|
||||
search.target = "/api/packages/alice/conan/v2/conans/search"
|
||||
|
||||
body = "q=x%2F1.or.1"
|
||||
if got, _ := inspectBody(t, readingBodies(t), search, body); got.Score < threshold {
|
||||
t.Errorf("POST %s %s let through: %+v", search.target, body, got)
|
||||
}
|
||||
|
||||
// The rule that lets a Conan pattern through is in phase 2, where it
|
||||
// checks tx.app_gitea itself: a hostname that does not serve gitea
|
||||
// refuses the pattern.
|
||||
pattern := get(
|
||||
"/api/packages/alice/conan/v2/conans/search?q=tools%2F%2A",
|
||||
)
|
||||
apps := servingApps(t, map[string][]string{
|
||||
giteaHost: {gitea}, otherHost: {wordpress},
|
||||
})
|
||||
wantResult(t, apps, withHost(pattern, giteaHost), coreruleset.Result{})
|
||||
wantResult(t, apps, withHost(pattern, otherHost), matched(942100))
|
||||
}
|
||||
Reference in new issue
Block a user