Core Rule Set: the package registries' searches of gitea and Forgejo pass (closes #354)
check / check (push) Successful in 4m35s
check / check (push) Successful in 4m35s
Recorded from gitea 1.27.3 and Forgejo 17.0.1 with each registry's own client, and curl for the Chef cookbook list, NuGet's Packages() and the counts at Search()/$count and Packages()/$count, 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 */, such as zlib/*, skip 942100; 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
23413eb00a
5 files changed
+448
-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,121 @@ 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=",
|
||||
}
|
||||
|
||||
// giteaRegistryNameSearches 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 giteaRegistryNameSearches = []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,
|
||||
giteaRegistryNameSearches,
|
||||
)
|
||||
}
|
||||
|
||||
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, and SQL comments that stand in for spaces.
|
||||
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",
|
||||
} {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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