Read every setting through viper and rename the settings (closes #211)
check / check (push) Waiting to run
check / check (push) Waiting to run
Every setting of waf and rproxy is read through viper, in place of the os.LookupEnv function Main passed down. NewViper turns AutomaticEnv and AllowEmptyEnv on, so a setting that is set but empty reads as set. A setting may also be given as a file, the _FILE form, which rproxy's settings now have too; rproxy still writes nothing to disk. The settings are renamed: waf's take the prefix SWW_WAF_, rproxy's take SWW_RPROXY_, and the shared SWW_COMMAND keeps SWW_. The Core Rule Set's settings take SWW_WAF_CORE_RULE_SET_ rather than SWW_WAF_WAF_. Model: deepseek-v4-pro (first version); opus-5-5 (rework)
This commit is contained in:
1 parent
75f30a34b2
commit
74b9f4ca74
128 files changed
+2436
-2253
No files matched your search
@@ -1,8 +1,8 @@
|
||||
// Package coreruleset runs the OWASP Core Rule Set 4.25.0, through Coraza, on the
|
||||
// method, the URL with its query and the headers of a request, and on its
|
||||
// body while SWW_WAF_BODY_LIMIT is set, with the five changes sww
|
||||
// makes to it, the fixes for the apps SWW_APPS names, and the exclusions in
|
||||
// SWW_CORE_RULE_SET_EXCLUSIONS_FILE, as "Attack detection" under "Settings"
|
||||
// body while SWW_WAF_CORE_RULE_SET_BODY_LIMIT is set, with the five changes sww
|
||||
// makes to it, the fixes for the apps SWW_WAF_APPS names, and the exclusions in
|
||||
// SWW_WAF_CORE_RULE_SET_EXCLUSIONS_FILE, as "Attack detection" under "Settings"
|
||||
// in SPEC.md describes them. It reads no response.
|
||||
//
|
||||
// sww writes only to its state directory, so Coraza is built with
|
||||
@@ -34,16 +34,16 @@ import (
|
||||
|
||||
// directives are the Core Rule Set as sww runs it, with the
|
||||
// paranoia level for %d, bodyDirectives for the first %s while
|
||||
// SWW_WAF_BODY_LIMIT is set, the exclusions file for the second, and the
|
||||
// directives of every app in apps for the third.
|
||||
// SWW_WAF_CORE_RULE_SET_BODY_LIMIT is set, the exclusions file for the second,
|
||||
// and the directives of every app in apps for the third.
|
||||
// Each rule sww adds has an id from 900000 to 900999, the ids the
|
||||
// Core Rule Set keeps for the rules that set it up, which
|
||||
// SWW_WAF_DISABLED_RULES and the exclusions file refuse, so that no
|
||||
// SWW_WAF_CORE_RULE_SET_DISABLED_RULES and the exclusions file refuse, so that no
|
||||
// setting switches one off. Coraza joins a line ending in \ to the next,
|
||||
// without the spaces at the start of the next.
|
||||
const directives = `
|
||||
# The engine only detects. sww compares the request's anomaly
|
||||
# score with SWW_WAF_ANOMALY_THRESHOLD itself, in block and detect mode
|
||||
# score with SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD itself, in block and detect mode
|
||||
# alike. It reads no body, unless bodyDirectives switch that on.
|
||||
SecRuleEngine DetectionOnly
|
||||
SecRequestBodyAccess Off
|
||||
@@ -185,7 +185,7 @@ var nextcloudDirectives string
|
||||
//go:embed forgejo.conf
|
||||
var forgejoDirectives string
|
||||
|
||||
// apps are the apps whose fixes sww has, by the name SWW_APPS gives each.
|
||||
// apps are the apps whose fixes sww has, by the name SWW_WAF_APPS gives each.
|
||||
// flash is Forgejo's name for gitea_flash.
|
||||
//
|
||||
//nolint:gochecknoglobals // a constant cannot be a map
|
||||
@@ -215,20 +215,20 @@ func AppNames() []string {
|
||||
|
||||
// Params are what New needs.
|
||||
type Params struct {
|
||||
// ParanoiaLevel is SWW_WAF_PARANOIA_LEVEL, from 1 to 4.
|
||||
// ParanoiaLevel is SWW_WAF_CORE_RULE_SET_PARANOIA_LEVEL, from 1 to 4.
|
||||
ParanoiaLevel int
|
||||
// DisabledRules are the ids of the rules switched off
|
||||
// (SWW_WAF_DISABLED_RULES).
|
||||
// (SWW_WAF_CORE_RULE_SET_DISABLED_RULES).
|
||||
DisabledRules []int
|
||||
// BodyLimit is the most of a request body the Core Rule Set reads
|
||||
// (SWW_WAF_BODY_LIMIT), 0 while it is off and it reads none.
|
||||
// (SWW_WAF_CORE_RULE_SET_BODY_LIMIT), 0 while it is off and it reads none.
|
||||
BodyLimit int64
|
||||
// ExclusionsFile is the path of a file of exclusions loaded before the
|
||||
// Core Rule Set's rules (SWW_CORE_RULE_SET_EXCLUSIONS_FILE), "" while
|
||||
// Core Rule Set's rules (SWW_WAF_CORE_RULE_SET_EXCLUSIONS_FILE), "" while
|
||||
// there is none.
|
||||
ExclusionsFile string
|
||||
// Apps are the names of the apps each hostname serves, by hostname in
|
||||
// lowercase (SWW_APPS). A request gets only the fixes of its hostname's
|
||||
// lowercase (SWW_WAF_APPS). A request gets only the fixes of its hostname's
|
||||
// apps and of the apps they are forks of, or of every app while Apps is
|
||||
// nil.
|
||||
Apps map[string][]string
|
||||
@@ -256,7 +256,7 @@ var errSwwRuleID = errors.New("an id of sww's own rules")
|
||||
// the directive finds none of them.
|
||||
var errChangesNoRule = errors.New(
|
||||
"the Core Rule Set's rules load after the file; " +
|
||||
"switch a rule off with SWW_WAF_DISABLED_RULES, or on some requests " +
|
||||
"switch a rule off with SWW_WAF_CORE_RULE_SET_DISABLED_RULES, or on some requests " +
|
||||
"with a ctl exclusion",
|
||||
)
|
||||
|
||||
@@ -335,7 +335,7 @@ type CoreRuleSet struct {
|
||||
engine coraza.WAF
|
||||
bodyLimit int64
|
||||
// appsByHostname are the names of the apps each hostname serves, by
|
||||
// hostname in lowercase (SWW_APPS), nil while SWW_APPS is unset.
|
||||
// hostname in lowercase (SWW_WAF_APPS), nil while SWW_WAF_APPS is unset.
|
||||
appsByHostname map[string][]string
|
||||
}
|
||||
|
||||
@@ -477,7 +477,7 @@ func (c *CoreRuleSet) Inspect(
|
||||
}
|
||||
|
||||
// readBody reads body, the body of the request in tx, which has run on
|
||||
// the request's headers, while SWW_WAF_BODY_LIMIT is set and the body is
|
||||
// the request's headers, while SWW_WAF_CORE_RULE_SET_BODY_LIMIT is set and the body is
|
||||
// of a kind the Core Rule Set reads: form data and multipart, of which it
|
||||
// reads the first c.bodyLimit bytes, and JSON and XML, which it reads only
|
||||
// when they are no longer than that, since they cannot be read in part.
|
||||
@@ -534,8 +534,8 @@ func score(tx types.Transaction) int {
|
||||
}
|
||||
|
||||
// appsFor returns the names of the apps whose fixes a request for host,
|
||||
// its Host header, gets: every app while SWW_APPS is unset, and otherwise
|
||||
// those SWW_APPS lists for host's name, without the port and in
|
||||
// its Host header, gets: every app while SWW_WAF_APPS is unset, and otherwise
|
||||
// those SWW_WAF_APPS lists for host's name, without the port and in
|
||||
// lowercase, each followed by the app it is a fork of, if any.
|
||||
func (c *CoreRuleSet) appsFor(host string) []string {
|
||||
if c.appsByHostname == nil {
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
|
||||
)
|
||||
|
||||
// defaultDisabledRules are the rules SWW_WAF_DISABLED_RULES switches off
|
||||
// defaultDisabledRules are the rules SWW_WAF_CORE_RULE_SET_DISABLED_RULES switches off
|
||||
// by default.
|
||||
//
|
||||
//nolint:gochecknoglobals // a constant cannot be a list
|
||||
@@ -650,17 +650,17 @@ var giteaFixed = []struct {
|
||||
{get("/v2/owner/app/blobs/uploads/?from=python3"), matched(932260)},
|
||||
}
|
||||
|
||||
// The hostnames of the tests of SWW_APPS: the one that serves gitea, and
|
||||
// The hostnames of the tests of SWW_WAF_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.
|
||||
// gitea is gitea's name in SWW_WAF_APPS.
|
||||
const gitea = "gitea"
|
||||
|
||||
// wordpress is WordPress's name in SWW_APPS.
|
||||
// wordpress is WordPress's name in SWW_WAF_APPS.
|
||||
const wordpress = "wordpress"
|
||||
|
||||
// withHost returns r sent to host.
|
||||
@@ -672,7 +672,7 @@ func withHost(r request, host string) request {
|
||||
}
|
||||
}
|
||||
|
||||
// servingApps returns the Core Rule Set at the defaults, with SWW_APPS
|
||||
// servingApps returns the Core Rule Set at the defaults, with SWW_WAF_APPS
|
||||
// giving apps.
|
||||
func servingApps(
|
||||
t *testing.T,
|
||||
@@ -725,7 +725,7 @@ func TestUnlistedHostnameGetsTheFullCheck(t *testing.T) {
|
||||
|
||||
for _, apps := range []map[string][]string{
|
||||
{giteaHost: {gitea}},
|
||||
// SWW_APPS set empty lists no hostname.
|
||||
// SWW_WAF_APPS set empty lists no hostname.
|
||||
{},
|
||||
} {
|
||||
crs := servingApps(t, apps)
|
||||
@@ -1162,7 +1162,7 @@ func misfiringPasswords(t *testing.T) []string {
|
||||
func wantRandomPasswordsPass(t *testing.T, r request, name string) {
|
||||
t.Helper()
|
||||
|
||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||
const threshold = 5 // SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD's default
|
||||
|
||||
crs := readingBodies(t)
|
||||
misfiring := misfiringPasswords(t)
|
||||
@@ -1193,7 +1193,7 @@ func wantRandomPasswordsPass(t *testing.T, r request, name string) {
|
||||
func wantPasswordAttacksPass(t *testing.T, r request, names ...string) {
|
||||
t.Helper()
|
||||
|
||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||
const threshold = 5 // SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD's default
|
||||
|
||||
for _, attack := range []string{
|
||||
"' OR '1'='1", pathTraversal, "; cat /etc/passwd",
|
||||
@@ -1213,7 +1213,7 @@ func wantPasswordAttacksPass(t *testing.T, r request, names ...string) {
|
||||
func wantPasswordAttacksRefused(t *testing.T, r request, name string) {
|
||||
t.Helper()
|
||||
|
||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||
const threshold = 5 // SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD's default
|
||||
|
||||
for _, attack := range []string{scriptTag, "javascript:alert(1)"} {
|
||||
body := passwordBody(t, r, []string{name}, attack)
|
||||
@@ -1303,7 +1303,7 @@ func TestGiteaPasswordsPass(t *testing.T) {
|
||||
func TestPasswordFieldsStayCheckedInTheQuery(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||
const threshold = 5 // SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD's default
|
||||
|
||||
crs := readingBodies(t)
|
||||
|
||||
@@ -1351,7 +1351,7 @@ func TestPasswordFieldsStayCheckedInTheQuery(t *testing.T) {
|
||||
func TestPasswordExclusionsAreNarrow(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||
const threshold = 5 // SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD's default
|
||||
|
||||
crs := readingBodies(t)
|
||||
|
||||
@@ -1387,7 +1387,7 @@ func TestPasswordExclusionsAreNarrow(t *testing.T) {
|
||||
func TestGiteaAttacksAreRefused(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||
const threshold = 5 // SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD's default
|
||||
|
||||
crs := atDefaults(t)
|
||||
|
||||
@@ -1467,7 +1467,7 @@ func TestGiteaSearchOnAUsersOrAnOrganisationsPageLetsLsThrough(
|
||||
}
|
||||
|
||||
// A command after a ";" in q is still refused on each of these paths.
|
||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||
const threshold = 5 // SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD's default
|
||||
|
||||
for _, target := range []string{
|
||||
"/alice?tab=repositories&q=",
|
||||
@@ -1492,7 +1492,7 @@ const (
|
||||
)
|
||||
|
||||
// readingBodiesUpTo64K returns the Core Rule Set as sww runs it by default,
|
||||
// but reading bodies up to the largest SWW_WAF_BODY_LIMIT.
|
||||
// but reading bodies up to the largest SWW_WAF_CORE_RULE_SET_BODY_LIMIT.
|
||||
func readingBodiesUpTo64K(t *testing.T) *coreruleset.CoreRuleSet {
|
||||
t.Helper()
|
||||
|
||||
@@ -1559,7 +1559,7 @@ func wordpressAttackTargets(attack string) []string {
|
||||
func TestWordPressAttacksAreRefused(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||
const threshold = 5 // SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD's default
|
||||
|
||||
defaults, bodies := atDefaults(t), readingBodiesUpTo64K(t)
|
||||
|
||||
@@ -2297,7 +2297,7 @@ func TestWordPressExclusionsAreNarrow(t *testing.T) {
|
||||
wantBody(t, crs, signIn, body, matched(930120), body)
|
||||
}
|
||||
|
||||
// bodyLimit is SWW_WAF_BODY_LIMIT in the tests that read bodies.
|
||||
// bodyLimit is SWW_WAF_CORE_RULE_SET_BODY_LIMIT in the tests that read bodies.
|
||||
const bodyLimit = 8 << 10
|
||||
|
||||
// The Content-Type headers of the kinds of body the Core Rule Set reads.
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
# request, before the rules it switches off, so this file is loaded before the
|
||||
# Core Rule Set's rules. Each comment names a request a rule refused. An
|
||||
# exclusion on a field of a body applies only when the query has no parameter
|
||||
# of that name, so it matters only while SWW_WAF_BODY_LIMIT is set. The
|
||||
# exclusions above the comment "The fields below are in bodies" also cover a
|
||||
# body field of the same name whatever the query holds, and only those below it
|
||||
# need a query without that name.
|
||||
# of that name, so it matters only while SWW_WAF_CORE_RULE_SET_BODY_LIMIT is
|
||||
# set. The exclusions above the comment "The fields below are in bodies" also
|
||||
# cover a body field of the same name whatever the query holds, and only those
|
||||
# below it need a query without that name.
|
||||
#
|
||||
# Each path may start with a language prefix, such as /fr in
|
||||
# /fr/admin/content. The prefix is two or three letters, then optionally a
|
||||
@@ -26,7 +26,7 @@
|
||||
# 932235: python3
|
||||
|
||||
# Every rule below is skipped unless Inspect has set tx.app_drupal. It sets it
|
||||
# while SWW_APPS is unset, or lists drupal for the request's hostname.
|
||||
# while SWW_WAF_APPS is unset, or lists drupal for the request's hostname.
|
||||
# The skip reaches only phase 1, so a rule of a later phase must check
|
||||
# tx.app_drupal itself.
|
||||
SecRule &TX:app_drupal "@eq 0" \
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
|
||||
)
|
||||
|
||||
// drupalThreshold is SWW_WAF_ANOMALY_THRESHOLD's default.
|
||||
// drupalThreshold is SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD's default.
|
||||
const drupalThreshold = 5
|
||||
|
||||
// drupalAddArticle is Drupal's form for a new article.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# rules. Each comment names a request the rule refused.
|
||||
|
||||
# Every rule below is skipped unless Inspect has set tx.app_forgejo. It sets it
|
||||
# while SWW_APPS is unset, or lists forgejo for the request's hostname.
|
||||
# while SWW_WAF_APPS is unset, or lists forgejo for the request's hostname.
|
||||
# The skip reaches only phase 1, so a rule of a later phase must check
|
||||
# tx.app_forgejo itself.
|
||||
SecRule &TX:app_forgejo "@eq 0" \
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
|
||||
)
|
||||
|
||||
// forgejo is Forgejo's name in SWW_APPS.
|
||||
// forgejo is Forgejo's name in SWW_WAF_APPS.
|
||||
const forgejo = "forgejo"
|
||||
|
||||
// forgejoHost is the hostname of the tests of SWW_APPS that serves Forgejo.
|
||||
// forgejoHost is the hostname of the tests of SWW_WAF_APPS that serves Forgejo.
|
||||
const forgejoHost = "code.example"
|
||||
|
||||
func TestForgejoFixesApplyOnlyToAHostnameThatServesForgejo(t *testing.T) {
|
||||
@@ -91,7 +91,7 @@ var forgejoAttacks = []string{
|
||||
func TestForgejoAttacksAreRefusedInItsSearches(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||
const threshold = 5 // SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD's default
|
||||
|
||||
defaults, bodies := atDefaults(t), readingBodies(t)
|
||||
|
||||
@@ -113,7 +113,7 @@ func TestForgejoAttacksAreRefusedInItsSearches(t *testing.T) {
|
||||
func TestForgejoExclusionsAreNarrow(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||
const threshold = 5 // SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD's default
|
||||
|
||||
crs := atDefaults(t)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# Core Rule Set's rules. Each comment names a request the rule refused.
|
||||
|
||||
# Every rule below is skipped unless Inspect has set tx.app_gitea. It sets it
|
||||
# while SWW_APPS is unset, or lists gitea or forgejo for the request's hostname.
|
||||
# while SWW_WAF_APPS is unset, or lists gitea or forgejo for the request's hostname.
|
||||
# The skip reaches only phase 1, so a rule of a later phase must check
|
||||
# tx.app_gitea itself.
|
||||
SecRule &TX:app_gitea "@eq 0" \
|
||||
|
||||
@@ -85,7 +85,7 @@ func TestGiteaAdminFormsLetHardValuesThroughInTheBody(t *testing.T) {
|
||||
func TestGiteaFormsThatAdoptARepositoryLetItsNameThrough(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||
const threshold = 5 // SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD's default
|
||||
|
||||
crs := readingBodies(t)
|
||||
|
||||
@@ -146,7 +146,7 @@ func TestGiteaFormsThatAdoptARepositoryLetItsNameThrough(t *testing.T) {
|
||||
func TestGiteaAttacksAreRefusedInItsSearches(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||
const threshold = 5 // SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD's default
|
||||
|
||||
defaults, bodies := atDefaults(t), readingBodies(t)
|
||||
|
||||
@@ -170,7 +170,7 @@ func TestGiteaAttacksAreRefusedInItsSearches(t *testing.T) {
|
||||
func TestGiteaSearchExclusionsAreNarrow(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||
const threshold = 5 // SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD's default
|
||||
|
||||
crs := atDefaults(t)
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
# exclusion runs with the request, before the rules it switches off, so this
|
||||
# file is loaded before the Core Rule Set's rules. Each comment names a request
|
||||
# a rule refused. An exclusion on a field of a body applies only when the query
|
||||
# has no parameter of that name, so it matters only while SWW_WAF_BODY_LIMIT is
|
||||
# set.
|
||||
# has no parameter of that name, so it matters only while
|
||||
# SWW_WAF_CORE_RULE_SET_BODY_LIMIT is set.
|
||||
#
|
||||
# Gitea serves /api/v1/ too, and Drupal serves /admin/. So only Mastodon's own
|
||||
# resources under them count as Mastodon's paths.
|
||||
@@ -18,7 +18,7 @@
|
||||
# 932260: python3
|
||||
|
||||
# Every rule below is skipped unless Inspect has set tx.app_mastodon. It sets
|
||||
# it while SWW_APPS is unset, or lists mastodon for the request's hostname.
|
||||
# it while SWW_WAF_APPS is unset, or lists mastodon for the request's hostname.
|
||||
# The skip reaches only phase 1, so a rule of a later phase must check
|
||||
# tx.app_mastodon itself.
|
||||
SecRule &TX:app_mastodon "@eq 0" \
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
|
||||
)
|
||||
|
||||
// mastodonThreshold is SWW_WAF_ANOMALY_THRESHOLD's default.
|
||||
// mastodonThreshold is SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD's default.
|
||||
const mastodonThreshold = 5
|
||||
|
||||
// mastodonStatuses is where Mastodon's apps and web app post.
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
# request, before the rules it switches off, so this file is loaded before the
|
||||
# Core Rule Set's rules. Each comment names a request a rule refused. An
|
||||
# exclusion on a field of a body applies only when the query has no parameter of
|
||||
# that name, so it matters only while SWW_WAF_BODY_LIMIT is set. MediaWiki's
|
||||
# paths are matched at the site root and under /w/.
|
||||
# that name, so it matters only while SWW_WAF_CORE_RULE_SET_BODY_LIMIT is set.
|
||||
# MediaWiki's paths are matched at the site root and under /w/.
|
||||
#
|
||||
# WordPress serves /index.php too. So /index.php counts as MediaWiki's only
|
||||
# with a page title, in the query or after the path:
|
||||
@@ -22,7 +22,7 @@
|
||||
# 932260: python3
|
||||
|
||||
# Every rule below is skipped unless Inspect has set tx.app_mediawiki. It sets
|
||||
# it while SWW_APPS is unset, or lists mediawiki for the request's hostname.
|
||||
# it while SWW_WAF_APPS is unset, or lists mediawiki for the request's hostname.
|
||||
# The skip reaches only phase 1, so a rule of a later phase must check
|
||||
# tx.app_mediawiki itself.
|
||||
SecRule &TX:app_mediawiki "@eq 0" \
|
||||
|
||||
@@ -9,10 +9,10 @@ import (
|
||||
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
|
||||
)
|
||||
|
||||
// mediawikiThreshold is SWW_WAF_ANOMALY_THRESHOLD's default.
|
||||
// mediawikiThreshold is SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD's default.
|
||||
const mediawikiThreshold = 5
|
||||
|
||||
// mediawiki is MediaWiki's name in SWW_APPS.
|
||||
// mediawiki is MediaWiki's name in SWW_WAF_APPS.
|
||||
const mediawiki = "mediawiki"
|
||||
|
||||
// mediawikiEdit is the address MediaWiki's edit form posts to.
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
# switches off, so this file is loaded before the Core Rule Set's rules. Each
|
||||
# comment names a request a rule refused. An exclusion on a field of a JSON body
|
||||
# applies only when the query has no parameter of that name, so it matters only
|
||||
# while SWW_WAF_BODY_LIMIT is set. Nextcloud's pages are matched at the site
|
||||
# root and under /index.php/, where a Nextcloud without pretty URLs serves them.
|
||||
# while SWW_WAF_CORE_RULE_SET_BODY_LIMIT is set. Nextcloud's pages are matched
|
||||
# at the site root and under /index.php/, where a Nextcloud without pretty URLs
|
||||
# serves them.
|
||||
#
|
||||
# Four rules read a search or a name as a command or a system file:
|
||||
# 930120: .htaccess
|
||||
@@ -30,7 +31,7 @@
|
||||
# injection, path traversal and script injection are still refused there.
|
||||
|
||||
# Every rule below is skipped unless Inspect has set tx.app_nextcloud. It sets
|
||||
# it while SWW_APPS is unset, or lists nextcloud for the request's hostname.
|
||||
# it while SWW_WAF_APPS is unset, or lists nextcloud for the request's hostname.
|
||||
# The skip reaches only phase 1, so a rule of a later phase must check
|
||||
# tx.app_nextcloud itself.
|
||||
SecRule &TX:app_nextcloud "@eq 0" \
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
|
||||
)
|
||||
|
||||
// nextcloudThreshold is SWW_WAF_ANOMALY_THRESHOLD's default.
|
||||
// nextcloudThreshold is SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD's default.
|
||||
const nextcloudThreshold = 5
|
||||
|
||||
// nextcloudSignIn is the sign-in page.
|
||||
@@ -51,7 +51,7 @@ const (
|
||||
// nextcloudOther is a JSON field no exclusion names.
|
||||
const nextcloudOther = `{"other":"python3"}`
|
||||
|
||||
// nextcloud is Nextcloud's name in SWW_APPS.
|
||||
// nextcloud is Nextcloud's name in SWW_WAF_APPS.
|
||||
const nextcloud = "nextcloud"
|
||||
|
||||
func TestNextcloudFixesApplyOnlyToAHostnameThatServesNextcloud(
|
||||
@@ -528,7 +528,7 @@ func TestNextcloudPasswordFieldsPass(t *testing.T) {
|
||||
func TestNextcloudUserPasswordInValuePasses(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||
const threshold = 5 // SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD's default
|
||||
|
||||
crs := readingBodies(t)
|
||||
target := nextcloudUsers + "/alice"
|
||||
|
||||
@@ -17,8 +17,8 @@ import (
|
||||
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
|
||||
)
|
||||
|
||||
// appsUnset labels the checks made while SWW_APPS is unset.
|
||||
const appsUnset = "SWW_APPS unset"
|
||||
// appsUnset labels the checks made while SWW_WAF_APPS is unset.
|
||||
const appsUnset = "SWW_WAF_APPS unset"
|
||||
|
||||
// giteaRequests returns the recorded ordinary gitea requests in testdata.
|
||||
// Each has the Referer of a page that names the same, as a page opened
|
||||
@@ -53,18 +53,18 @@ func giteaRequests(t *testing.T) []request {
|
||||
}
|
||||
|
||||
// TestOrdinaryGiteaRequestsPass checks gitea's recorded requests with
|
||||
// SWW_APPS unset, and on giteaHost while SWW_APPS lists only gitea for it.
|
||||
// SWW_WAF_APPS unset, and on giteaHost while SWW_WAF_APPS lists only gitea for it.
|
||||
func TestOrdinaryGiteaRequestsPass(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||
const threshold = 5 // SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD's default
|
||||
|
||||
requests := giteaRequests(t)
|
||||
giteaOnly := map[string][]string{giteaHost: {gitea}}
|
||||
|
||||
for apps, crs := range map[string]*coreruleset.CoreRuleSet{
|
||||
appsUnset: atDefaults(t),
|
||||
"SWW_APPS=" + giteaHost + ":" + gitea: servingApps(t, giteaOnly),
|
||||
appsUnset: atDefaults(t),
|
||||
"SWW_WAF_APPS=" + giteaHost + ":" + gitea: servingApps(t, giteaOnly),
|
||||
} {
|
||||
for _, r := range requests {
|
||||
got := inspect(t, crs, withHost(r, giteaHost))
|
||||
@@ -122,24 +122,24 @@ func recordedRequests(t *testing.T, name string) []recordedRequest {
|
||||
}
|
||||
|
||||
// wantRecordedRequestsPass checks that none of the recorded ordinary
|
||||
// requests in the file name in testdata is refused: with SWW_APPS unset, and
|
||||
// requests in the file name in testdata is refused: with SWW_WAF_APPS unset, and
|
||||
// on the hostname where they were recorded, otherHost for WordPress and
|
||||
// example.com for the other apps, while SWW_APPS lists only app for it. Both
|
||||
// example.com for the other apps, while SWW_WAF_APPS lists only app for it. Both
|
||||
// times, they are checked at the defaults and while bodies are read. A
|
||||
// request with neither a body nor a Content-Type header is checked only at
|
||||
// the defaults: with neither, the set that reads bodies has nothing more to
|
||||
// check. Each request runs as its own parallel subtest for each setting of
|
||||
// SWW_APPS.
|
||||
// SWW_WAF_APPS.
|
||||
func wantRecordedRequestsPass(t *testing.T, name, app string) {
|
||||
t.Helper()
|
||||
|
||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||
const threshold = 5 // SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD's default
|
||||
|
||||
requests := recordedRequests(t, name)
|
||||
|
||||
for setting, apps := range map[string]map[string][]string{
|
||||
appsUnset: nil,
|
||||
"SWW_APPS lists only " + app: {"example.com": {app}, otherHost: {app}},
|
||||
appsUnset: nil,
|
||||
"SWW_WAF_APPS lists only " + app: {"example.com": {app}, otherHost: {app}},
|
||||
} {
|
||||
defaults := servingApps(t, apps)
|
||||
bodies := shared(t, coreruleset.Params{
|
||||
@@ -204,22 +204,22 @@ func TestOrdinaryDrupalRequestsPass(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestOrdinaryForgejoRequestsPass checks Forgejo's recorded requests at the
|
||||
// defaults only: with SWW_APPS unset, and on example.com, where they were
|
||||
// recorded, while SWW_APPS lists only forgejo for it. While bodies are read,
|
||||
// defaults only: with SWW_WAF_APPS unset, and on example.com, where they were
|
||||
// recorded, while SWW_WAF_APPS lists only forgejo for it. While bodies are read,
|
||||
// the Core Rule Set still refuses the file and branch names some of them
|
||||
// carry in a form or JSON body, as it does gitea's
|
||||
// (TestGiteaBodiesTheCoreRuleSetRefuses).
|
||||
func TestOrdinaryForgejoRequestsPass(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const threshold = 5 // SWW_WAF_ANOMALY_THRESHOLD's default
|
||||
const threshold = 5 // SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD's default
|
||||
|
||||
requests := recordedRequests(t, "forgejo-requests.txt")
|
||||
forgejoOnly := map[string][]string{"example.com": {forgejo}}
|
||||
|
||||
for apps, crs := range map[string]*coreruleset.CoreRuleSet{
|
||||
appsUnset: atDefaults(t),
|
||||
"SWW_APPS=example.com:forgejo": servingApps(t, forgejoOnly),
|
||||
appsUnset: atDefaults(t),
|
||||
"SWW_WAF_APPS=example.com:forgejo": servingApps(t, forgejoOnly),
|
||||
} {
|
||||
for _, recorded := range requests {
|
||||
r := request{recorded.Method, recorded.Target, recorded.Headers}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# before the rules it switches off, so this file is loaded before the Core Rule
|
||||
# Set's rules. Each comment names a request a rule refused. An exclusion on a
|
||||
# field of a body applies only when the query has no parameter of that name, so
|
||||
# it matters only while SWW_WAF_BODY_LIMIT is set.
|
||||
# it matters only while SWW_WAF_CORE_RULE_SET_BODY_LIMIT is set.
|
||||
#
|
||||
# The apps encrypt the names, notes, addresses and file names of vault items,
|
||||
# folders, collections and sends before they send them. These values pass
|
||||
@@ -18,7 +18,7 @@
|
||||
# 932260: python3
|
||||
|
||||
# Every rule below is skipped unless Inspect has set tx.app_vaultwarden. It
|
||||
# sets it while SWW_APPS is unset, or lists vaultwarden for the request's
|
||||
# sets it while SWW_WAF_APPS is unset, or lists vaultwarden for the request's
|
||||
# hostname.
|
||||
# The skip reaches only phase 1, so a rule of a later phase must check
|
||||
# tx.app_vaultwarden itself.
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"sneak.berlin/go/smallwebwaf/internal/coreruleset"
|
||||
)
|
||||
|
||||
// vaultwardenThreshold is SWW_WAF_ANOMALY_THRESHOLD's default.
|
||||
// vaultwardenThreshold is SWW_WAF_CORE_RULE_SET_ANOMALY_THRESHOLD's default.
|
||||
const vaultwardenThreshold = 5
|
||||
|
||||
// Paths of Vaultwarden's API and admin page.
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
# the rules it switches off, so this file is loaded before the Core Rule Set's
|
||||
# rules. Each comment names a request the rule refused. An exclusion on a field
|
||||
# of a body applies only when the query has no parameter of that name, so it
|
||||
# matters only while SWW_WAF_BODY_LIMIT is set.
|
||||
# matters only while SWW_WAF_CORE_RULE_SET_BODY_LIMIT is set.
|
||||
|
||||
# Every rule below is skipped unless Inspect has set tx.app_wordpress. It sets
|
||||
# it while SWW_APPS is unset, or lists wordpress for the request's hostname.
|
||||
# it while SWW_WAF_APPS is unset, or lists wordpress for the request's hostname.
|
||||
# The skip reaches only phase 1, so a rule of a later phase must check
|
||||
# tx.app_wordpress itself.
|
||||
SecRule &TX:app_wordpress "@eq 0" \
|
||||
|
||||
Reference in new issue
Block a user