SWW_APPS: an app's Core Rule Set fixes apply only to the hostnames that serve it (closes #220)
check / check (push) Failing after 2m47s
check / check (push) Failing after 2m47s
Each app's fixes are one .conf file, whose first rule skips the rest unless Inspect sets tx.app_<name>, and one entry in apps. SWW_APPS names each hostname's apps. A hostname gets only its apps' fixes; while SWW_APPS is unset, every hostname gets every app's fixes. A hostname in SWW_APPS that is not a DNS name, such as *.example.com, stops the start. Unset, SWW_APPS is logged at start as null; set empty, as "". Model: opus-5-5
This commit is contained in:
1 parent
c1bb813a5e
commit
2d6a91c7b1
15 files changed
+892
-358
No files matched your search
@@ -36,12 +36,14 @@ var loaded = struct {
|
||||
}{loaders: map[string]func() (*coreruleset.CoreRuleSet, error){}}
|
||||
|
||||
// loaderFor returns the function that loads the Core Rule Set with params
|
||||
// once, making it the first time params are asked for.
|
||||
// once, making it the first time params are asked for. %#v writes a nil
|
||||
// Apps, which gives every app's fixes, apart from an empty one, which
|
||||
// gives none.
|
||||
func loaderFor(params coreruleset.Params) func() (*coreruleset.CoreRuleSet, error) {
|
||||
loaded.Lock()
|
||||
defer loaded.Unlock()
|
||||
|
||||
settings := fmt.Sprintf("%+v", params)
|
||||
settings := fmt.Sprintf("%#v", params)
|
||||
|
||||
load, found := loaded.loaders[settings]
|
||||
if !found {
|
||||
@@ -202,8 +204,8 @@ func atDefaults(t *testing.T) *coreruleset.CoreRuleSet {
|
||||
}
|
||||
|
||||
// wantChange checks that crs lets through passes, a request one of the
|
||||
// eleven changes is for, and still finds result in refused, a request like
|
||||
// it that the change is not for.
|
||||
// six changes or an app's fixes are for, and still finds result in
|
||||
// refused, a request like it that they are not for.
|
||||
func wantChange(
|
||||
t *testing.T, crs *coreruleset.CoreRuleSet, passes, refused request,
|
||||
result coreruleset.Result,
|
||||
@@ -464,6 +466,108 @@ func TestImagePushThatReusesALayerIsNotCheckedForCommandNames(t *testing.T) {
|
||||
matched(930120, 932160))
|
||||
}
|
||||
|
||||
// giteaFixed are gitea requests that only gitea's fixes let through, one
|
||||
// for each kind of fix, each with what the Core Rule Set finds in it
|
||||
// without them.
|
||||
//
|
||||
//nolint:gochecknoglobals // a constant cannot be a list
|
||||
var giteaFixed = []struct {
|
||||
request request
|
||||
result coreruleset.Result
|
||||
}{
|
||||
// A search of code, on the path of gitea's search.
|
||||
{get("/explore/code?q=SELECT%20*%20FROM%20users%20WHERE"), matched(942100)},
|
||||
// A file name, in a parameter gitea sends names in, on any path.
|
||||
{get("/?path=.gitignore"), matched(930120)},
|
||||
// A file name, in a cookie of gitea's.
|
||||
{
|
||||
get("/owner/repo", "Cookie: gitea_flash=success%3DFile%2Bpackage.json%2Bdeleted"),
|
||||
matched(930120),
|
||||
},
|
||||
}
|
||||
|
||||
// The hostnames of the tests of SWW_APPS: the one that serves gitea, and
|
||||
// one that serves another app.
|
||||
const (
|
||||
giteaHost = "git.example"
|
||||
otherHost = "blog.example"
|
||||
)
|
||||
|
||||
// gitea is gitea's name in SWW_APPS.
|
||||
const gitea = "gitea"
|
||||
|
||||
// withHost returns r sent to host.
|
||||
func withHost(r request, host string) request {
|
||||
return request{r.method, r.target, append([]string{"Host: " + host}, r.headers...)}
|
||||
}
|
||||
|
||||
// servingApps returns the Core Rule Set at the defaults, with SWW_APPS
|
||||
// giving apps.
|
||||
func servingApps(t *testing.T, apps map[string][]string) *coreruleset.CoreRuleSet {
|
||||
t.Helper()
|
||||
|
||||
return shared(t, coreruleset.Params{
|
||||
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, Apps: apps,
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppsUnsetGivesEveryHostnameEveryAppsFixes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
crs := servingApps(t, nil)
|
||||
|
||||
for _, fixed := range giteaFixed {
|
||||
for _, host := range []string{giteaHost, otherHost} {
|
||||
wantResult(t, crs, withHost(fixed.request, host), coreruleset.Result{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestListedHostnameGetsItsAppsFixes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
crs := servingApps(t, map[string][]string{giteaHost: {gitea}})
|
||||
|
||||
// The hostname is matched without the port and without regard to case.
|
||||
for _, host := range []string{giteaHost, "GIT.Example:8443"} {
|
||||
for _, fixed := range giteaFixed {
|
||||
wantResult(t, crs, withHost(fixed.request, host), coreruleset.Result{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnlistedHostnameGetsTheFullCheck(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, apps := range []map[string][]string{
|
||||
{giteaHost: {gitea}},
|
||||
// SWW_APPS set empty lists no hostname.
|
||||
{},
|
||||
} {
|
||||
crs := servingApps(t, apps)
|
||||
|
||||
for _, host := range []string{otherHost, otherHost + ":8443", "git.example.org"} {
|
||||
for _, fixed := range giteaFixed {
|
||||
wantResult(t, crs, withHost(fixed.request, host), fixed.result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWordPressFixesApplyOnlyToAHostnameThatServesWordPress(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
crs := servingApps(t, map[string][]string{
|
||||
giteaHost: {gitea}, otherHost: {"wordpress"},
|
||||
})
|
||||
|
||||
// A search, which only WordPress's fixes let through.
|
||||
search := get("/?s=python3")
|
||||
|
||||
wantResult(t, crs, withHost(search, otherHost), coreruleset.Result{})
|
||||
wantResult(t, crs, withHost(search, giteaHost), matched(932260))
|
||||
}
|
||||
|
||||
// withExclusions returns the Core Rule Set at the defaults, with the
|
||||
// exclusions file at path.
|
||||
func withExclusions(path string) (*coreruleset.CoreRuleSet, error) {
|
||||
@@ -619,15 +723,16 @@ const (
|
||||
var hardValues = []string{"python3", "powershell", "ls%20-la", ".htaccess"}
|
||||
|
||||
// wantRecordedRequestsPass checks that none of the recorded ordinary
|
||||
// requests in the file name in testdata is refused, at the defaults and
|
||||
// while bodies are read. Each request runs as its own parallel subtest.
|
||||
func wantRecordedRequestsPass(t *testing.T, name string) {
|
||||
// requests in the file name in testdata is refused by defaults, which
|
||||
// reads no body, or by bodies, which reads it. Each request runs as its
|
||||
// own parallel subtest.
|
||||
func wantRecordedRequestsPass(
|
||||
t *testing.T, name string, defaults, bodies *coreruleset.CoreRuleSet,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||
|
||||
defaults, bodies := atDefaults(t), readingBodies(t)
|
||||
|
||||
for _, recorded := range recordedRequests(t, name) {
|
||||
t.Run(recorded.Method+" "+recorded.Target, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -791,13 +896,16 @@ func wordpressAttackTargets(attack string) []string {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrdinaryWordPressRequestsPassAndAttacksAreRefused(t *testing.T) {
|
||||
t.Parallel()
|
||||
// wantWordPressRequestsPass checks that the recorded ordinary WordPress
|
||||
// requests pass defaults, which reads no body, and that those with a body
|
||||
// pass bodies, which reads it.
|
||||
func wantWordPressRequestsPass(
|
||||
t *testing.T, defaults, bodies *coreruleset.CoreRuleSet,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||
|
||||
defaults, bodies := atDefaults(t), readingBodiesUpTo64K(t)
|
||||
|
||||
for _, recorded := range recordedRequests(t, "wordpress-requests.txt") {
|
||||
t.Run(recorded.Method+" "+recorded.Target, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -817,6 +925,16 @@ func TestOrdinaryWordPressRequestsPassAndAttacksAreRefused(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrdinaryWordPressRequestsPassAndAttacksAreRefused(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||
|
||||
defaults, bodies := atDefaults(t), readingBodiesUpTo64K(t)
|
||||
|
||||
wantWordPressRequestsPass(t, defaults, bodies)
|
||||
|
||||
for _, attack := range []string{
|
||||
sqlInjection,
|
||||
@@ -893,6 +1011,20 @@ func wordpressWords() []struct {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostnameListingWordPressLetsItsRecordedRequestsThrough(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// The requests were recorded on otherHost.
|
||||
apps := map[string][]string{otherHost: {"wordpress"}}
|
||||
|
||||
bodies := shared(t, coreruleset.Params{
|
||||
ParanoiaLevel: 1, DisabledRules: defaultDisabledRules, BodyLimit: 64<<10 - 1,
|
||||
Apps: apps,
|
||||
})
|
||||
|
||||
wantWordPressRequestsPass(t, servingApps(t, apps), bodies)
|
||||
}
|
||||
|
||||
// wordpressBlock is the comment that starts every block of WordPress's
|
||||
// HTML, which rule 941180 matches.
|
||||
const wordpressBlock = "<!-- wp:paragraph -->"
|
||||
|
||||
Reference in new issue
Block a user