Files
smallwebwaf/internal/coreruleset/forgejo_test.go
T
clawbot 4d4ae61abf
check / check (push) Waiting to run
Rename the settings whose names do not say what they control (closes #234)
Applies the setting renames of the naming audit under the SWW_WAF_ and
SWW_RPROXY_ prefixes, with sneak's names for the IP reputation cache
time and the two time limits, which now take milliseconds. The rproxy
log field default_disposition becomes waf_backend_no_answer, and
config.Config and config.FromEnvironment become config.WAF and
config.WAFFromEnvironment. Go fields, test names and prose follow. No
aliases; nothing reads the old names.

Model: opus-5-5
2026-11-12 09:48:09 +00:00

170 lines
5.1 KiB
Go

package coreruleset_test
import (
"net/http"
"slices"
"testing"
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
)
// forgejo is Forgejo's name in SWW_WAF_APPS.
const forgejo = "forgejo"
// forgejoHost is the hostname of the tests of SWW_WAF_APPS that serves Forgejo.
const forgejoHost = "code.example"
func TestForgejoFixesApplyOnlyToAHostnameThatServesForgejo(t *testing.T) {
t.Parallel()
crs := servingApps(t, map[string][]string{
forgejoHost: {forgejo}, otherHost: {wordpress},
})
// A search of a wiki, a search of the admin page, and a file name in
// Forgejo's flash cookie, which only Forgejo's fixes let through.
for _, fixed := range []struct {
request request
result coreruleset.Result
}{
{get("/alice/tools/wiki/search?q=python3"), matched(932260)},
{get("/admin/repos?q=python3"), matched(932260)},
{
get("/alice/tools", "Cookie: flash=success%3DFile%2Bpackage.json%2Bdeleted"),
matched(930120),
},
} {
wantResult(
t,
crs,
withHost(fixed.request, forgejoHost),
coreruleset.Result{},
)
wantResult(t, crs, withHost(fixed.request, otherHost), fixed.result)
}
}
func TestHostnameListingForgejoGetsGiteasFixesToo(t *testing.T) {
t.Parallel()
crs := servingApps(t, map[string][]string{forgejoHost: {forgejo}})
for _, fixed := range giteaFixed {
wantResult(
t,
crs,
withHost(fixed.request, forgejoHost),
coreruleset.Result{},
)
}
}
// forgejoSearches are Forgejo's searches whose q skips some rules: those of
// a wiki, of releases, of a job's log and of the admin page, from Forgejo's
// fixes, and the searches for a name, from gitea's. The search of code at a
// branch or tag is not one of them: there, as on gitea's search of code, q
// lets these attacks through.
//
//nolint:gochecknoglobals // a constant cannot be a list
var forgejoSearches = []string{
"/alice/tools/wiki/search",
"/alice/tools/releases",
"/api/v1/repos/alice/tools/releases",
"/api/v1/repos/alice/tools/actions/jobs/1/logs",
"/admin/repos",
"/explore/repos",
"/explore/users",
"/alice/tools/branches",
"/alice/-/packages",
"/api/v1/packages/alice",
"/alice",
}
// forgejoAttacks are an SQL injection, a path traversal, a script injection
// and a shell command after a ";", as query values.
//
//nolint:gochecknoglobals // a constant cannot be a list
var forgejoAttacks = []string{
sqlInjection, "..%2Fcustom%2Fconf%2Fapp.ini", scriptInjection, commandInjection,
}
func TestForgejoAttacksAreRefusedInItsSearches(t *testing.T) {
t.Parallel()
const threshold = 5 // SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default
defaults, bodies := atDefaults(t), readingBodies(t)
for _, path := range forgejoSearches {
for _, attack := range forgejoAttacks {
if got := inspect(t, defaults, get(path+"?q="+attack)); got.Score < threshold {
t.Errorf("GET %s?q=%s let through: %+v", path, attack, got)
}
// The same field in a form data body.
r := request{http.MethodPost, path, []string{formData}}
if got, _ := inspectBody(t, bodies, r, "q="+attack); got.Score < threshold {
t.Errorf("POST %s q=%s let through: %+v", path, attack, got)
}
}
}
}
func TestForgejoExclusionsAreNarrow(t *testing.T) {
t.Parallel()
const threshold = 5 // SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default
crs := atDefaults(t)
// /bin/sh and the hard values, which q lets through on the searches
// named in Forgejo's fixes and on those of branches and packages, stay
// refused in q on paths near them and in another field on them.
// /alice/wiki/search would be the code search of a repository named
// wiki, where gitea's fixes let q through. Forgejo has no owner
// named api, so the paths under /api/ here are not searches.
for _, target := range []string{
"/alice/tools/wiki/Home?q=",
"/alice/tools/wiki/search/x?q=",
"/api/wiki/search?q=",
"/alice/tools/wiki/search?other=",
"/alice/tools/search/branch?q=",
"/alice/tools/search/commit/0123abc?q=",
"/alice/tools/src/branch/main?q=",
"/api/tools/search/branch/main?q=",
"/alice/tools/search/branch/main?other=",
"/alice/tools/releases/tag/v1.0?q=",
"/alice/tools/releases/latest?q=",
"/api/v1/releases?q=",
"/alice/tools/releases?other=",
"/api/v1/repos/alice/tools/actions/jobs/1?q=",
"/api/v1/repos/alice/tools/actions/runs/1/logs?q=",
"/alice/tools/actions/runs/1/jobs/0/logs?q=",
"/api/v1/repos/alice/tools/actions/jobs/1/logs?other=",
"/alice/tools/branches/x?q=",
"/alice/-/packages/npm/x?q=",
"/alice/tools/branches?other=",
} {
for _, value := range slices.Concat([]string{"%2Fbin%2Fsh"}, hardValues) {
if got := inspect(t, crs, get(target+value)); got.Score < threshold {
t.Errorf("%s%s let through: %+v", target, value, got)
}
}
}
// Referer keeps its rules on a page opened from a path near the code
// search at a branch or tag.
for _, path := range []string{
"/alice/tools/search/branch",
"/alice/tools/search/commit/0123abc",
"/api/tools/search/branch/main",
} {
for _, attack := range []string{scriptTag, injection} {
referer := "Referer: http://example.com" + path + "?q=" + attack
if got := inspect(t, crs, get("/", referer)); got.Score < threshold {
t.Errorf("%s let through: %+v", referer, got)
}
}
}
}