Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
365e1a3e01 |
No files matched your search
@@ -681,8 +681,12 @@ through. An app's fixes hold for the hostnames `SWW_APPS` lists the app for:
|
|||||||
Set empty, `SWW_APPS` lists no hostname.
|
Set empty, `SWW_APPS` lists no hostname.
|
||||||
- Unset, every app's fixes hold for every hostname. An app's fixes then weaken
|
- Unset, every app's fixes hold for every hostname. An app's fixes then weaken
|
||||||
the check of any other app that serves the same paths or fields.
|
the check of any other app that serves the same paths or fields.
|
||||||
- The start stops on an unknown app, a hostname listed twice, an app listed
|
- Any of these mistakes stops the start:
|
||||||
twice for one hostname, or an entry without a colon.
|
- an unknown app;
|
||||||
|
- a hostname that is not a DNS name, such as `*.example.com`;
|
||||||
|
- a hostname listed twice;
|
||||||
|
- an app listed twice for one hostname;
|
||||||
|
- an entry without a colon.
|
||||||
|
|
||||||
`gitea` is the only app supported. Each app's fixes are in one file, such as
|
`gitea` is the only app supported. Each app's fixes are in one file, such as
|
||||||
`internal/waf/gitea.conf`, and one entry of `apps` in `internal/waf/waf.go`.
|
`internal/waf/gitea.conf`, and one entry of `apps` in `internal/waf/waf.go`.
|
||||||
|
|||||||
+6
-2
@@ -210,8 +210,12 @@ between the two (see "The image's command" below).
|
|||||||
- A hostname not listed gets no app's fixes, so every rule checks it in
|
- A hostname not listed gets no app's fixes, so every rule checks it in
|
||||||
full. Set empty, it lists no hostname.
|
full. Set empty, it lists no hostname.
|
||||||
- Unset, every app's fixes apply to every hostname.
|
- Unset, every app's fixes apply to every hostname.
|
||||||
- An unknown app, a hostname listed twice, an app listed twice for one
|
- Any of these mistakes stops the start:
|
||||||
hostname, or an entry without a colon stops the start.
|
- an unknown app;
|
||||||
|
- a hostname that is not a DNS name, such as `*.example.com`;
|
||||||
|
- a hostname listed twice;
|
||||||
|
- an app listed twice for one hostname;
|
||||||
|
- an entry without a colon.
|
||||||
- `SWW_ERROR_BURST_THRESHOLD` (default `30`): the most refusals of one client in
|
- `SWW_ERROR_BURST_THRESHOLD` (default `30`): the most refusals of one client in
|
||||||
a minute (see "Attack detection" in `SPEC.md`). A count or `off`.
|
a minute (see "Attack detection" in `SPEC.md`). A count or `off`.
|
||||||
|
|
||||||
|
|||||||
@@ -406,6 +406,7 @@ var (
|
|||||||
"is not a path starting with / and without a ?, such as /wp-login.php")
|
"is not a path starting with / and without a ?, such as /wp-login.php")
|
||||||
errNotAppsEntry = errors.New(
|
errNotAppsEntry = errors.New(
|
||||||
"is not a hostname, : and a list of apps, such as git.example.com:gitea")
|
"is not a hostname, : and a list of apps, such as git.example.com:gitea")
|
||||||
|
errNotHostname = errors.New("is not a DNS name such as git.example.com")
|
||||||
errNotApp = errors.New("is not an app sww has fixes for; the apps are")
|
errNotApp = errors.New("is not an app sww has fixes for; the apps are")
|
||||||
errNotWAFMode = errors.New("is not off, detect or block")
|
errNotWAFMode = errors.New("is not off, detect or block")
|
||||||
errNotParanoiaLevel = errors.New("is not a paranoia level, from 1 to 4")
|
errNotParanoiaLevel = errors.New("is not a paranoia level, from 1 to 4")
|
||||||
@@ -1675,9 +1676,10 @@ func parseTrapPaths(value string) ([]string, error) {
|
|||||||
// parseApps reads the apps each hostname serves: entries separated by
|
// parseApps reads the apps each hostname serves: entries separated by
|
||||||
// semicolons, each a hostname, a colon, and a comma-separated list of the
|
// semicolons, each a hostname, a colon, and a comma-separated list of the
|
||||||
// names waf.AppNames returns, such as
|
// names waf.AppNames returns, such as
|
||||||
// git.example.com:gitea;cloud.example.com:nextcloud,wordpress. It returns
|
// git.example.com:gitea;cloud.example.com:nextcloud,wordpress. Each
|
||||||
// them by hostname in lowercase, since a hostname is matched without
|
// hostname is a DNS name, without a wildcard. It returns them by hostname
|
||||||
// regard to case. An empty value lists no hostname.
|
// in lowercase, since a hostname is matched without regard to case. An
|
||||||
|
// empty value lists no hostname.
|
||||||
func parseApps(value string) (map[string][]string, error) {
|
func parseApps(value string) (map[string][]string, error) {
|
||||||
apps := map[string][]string{}
|
apps := map[string][]string{}
|
||||||
|
|
||||||
@@ -1696,6 +1698,10 @@ func parseApps(value string) (map[string][]string, error) {
|
|||||||
return nil, fmt.Errorf("%q %w", entry, errNotAppsEntry)
|
return nil, fmt.Errorf("%q %w", entry, errNotAppsEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !isZone(hostname) {
|
||||||
|
return nil, fmt.Errorf("%q %w", hostname, errNotHostname)
|
||||||
|
}
|
||||||
|
|
||||||
if _, listed := apps[hostname]; listed {
|
if _, listed := apps[hostname]; listed {
|
||||||
return nil, fmt.Errorf("%q %w", hostname, errListedTwice)
|
return nil, fmt.Errorf("%q %w", hostname, errListedTwice)
|
||||||
}
|
}
|
||||||
@@ -2152,7 +2158,8 @@ func MaskZoneKey(zone string) string {
|
|||||||
return masked + "." + rest
|
return masked + "." + rest
|
||||||
}
|
}
|
||||||
|
|
||||||
// isZone reports whether each label of zone is as parseZones takes it.
|
// isZone reports whether each label of zone is as parseZones takes it. It
|
||||||
|
// also checks the hostnames of SWW_APPS, which are DNS names too.
|
||||||
func isZone(zone string) bool {
|
func isZone(zone string) bool {
|
||||||
for label := range strings.SplitSeq(zone, ".") {
|
for label := range strings.SplitSeq(zone, ".") {
|
||||||
badChar := strings.ContainsFunc(label, func(char rune) bool {
|
badChar := strings.ContainsFunc(label, func(char rune) bool {
|
||||||
|
|||||||
@@ -603,14 +603,20 @@ func TestApps(t *testing.T) {
|
|||||||
func TestInvalidAppsStopsTheStart(t *testing.T) {
|
func TestInvalidAppsStopsTheStart(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const notEntry = " is not a hostname, : and a list of apps, " +
|
const (
|
||||||
|
notEntry = " is not a hostname, : and a list of apps, " +
|
||||||
"such as git.example.com:gitea"
|
"such as git.example.com:gitea"
|
||||||
|
notHostname = " is not a DNS name such as git.example.com"
|
||||||
|
)
|
||||||
|
|
||||||
for _, tc := range []struct{ value, want string }{
|
for _, tc := range []struct{ value, want string }{
|
||||||
{"git.example.com:gitea;blog.example.com", `"blog.example.com"` + notEntry},
|
{"git.example.com:gitea;blog.example.com", `"blog.example.com"` + notEntry},
|
||||||
{"git.example.com:gitea;", `""` + notEntry},
|
{"git.example.com:gitea;", `""` + notEntry},
|
||||||
{":gitea", `":gitea"` + notEntry},
|
{":gitea", `":gitea"` + notEntry},
|
||||||
{"git.example.com:", `"git.example.com:"` + notEntry},
|
{"git.example.com:", `"git.example.com:"` + notEntry},
|
||||||
|
{"*.example.com:gitea", `"*.example.com"` + notHostname},
|
||||||
|
{"git.example.com/:gitea", `"git.example.com/"` + notHostname},
|
||||||
|
{"git example.com:gitea", `"git example.com"` + notHostname},
|
||||||
{
|
{
|
||||||
"git.example.com:gitea,nextcloud",
|
"git.example.com:gitea,nextcloud",
|
||||||
`"nextcloud" is not an app sww has fixes for; the apps are gitea`,
|
`"nextcloud" is not an app sww has fixes for; the apps are gitea`,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
||||||
|
"sneak.berlin/go/smallwebwaf/internal/discriminator"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
)
|
)
|
||||||
@@ -21,6 +22,7 @@ const (
|
|||||||
wafDisabledRules = "SWW_WAF_DISABLED_RULES"
|
wafDisabledRules = "SWW_WAF_DISABLED_RULES"
|
||||||
wafExemptPaths = "SWW_WAF_EXEMPT_PATHS"
|
wafExemptPaths = "SWW_WAF_EXEMPT_PATHS"
|
||||||
wafBodyLimit = "SWW_WAF_BODY_LIMIT"
|
wafBodyLimit = "SWW_WAF_BODY_LIMIT"
|
||||||
|
apps = "SWW_APPS"
|
||||||
block = "block"
|
block = "block"
|
||||||
detect = "detect"
|
detect = "detect"
|
||||||
)
|
)
|
||||||
@@ -237,6 +239,27 @@ func TestDisabledRulesSwitchOffWhatGiteaWouldBeRefused(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGiteasFixesApplyOnlyToAHostnameThatServesGitea(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Only gitea's fixes let a file name through in path, a parameter
|
||||||
|
// gitea sends file names in.
|
||||||
|
const fileName = "/?path=.gitignore"
|
||||||
|
|
||||||
|
s := startDiscriminator(t, "", map[string]string{
|
||||||
|
wafMode: block, apps: siteA + ":gitea",
|
||||||
|
})
|
||||||
|
|
||||||
|
wantDisposition(t, do(t, s.question(siteA, client, fileName)),
|
||||||
|
http.StatusOK, discriminator.DispositionAllow)
|
||||||
|
wantDisposition(t, do(t, s.question(siteB, client, fileName)),
|
||||||
|
http.StatusForbidden, discriminator.DispositionDeny)
|
||||||
|
|
||||||
|
lines := s.out.requestLines(t, 2)
|
||||||
|
wantLine(t, lines[0], http.StatusOK, requestlog.ActionForward)
|
||||||
|
wantLine(t, lines[1], http.StatusForbidden, requestlog.ActionWAFBlocked)
|
||||||
|
}
|
||||||
|
|
||||||
func TestAttackInAFormBodyIsRefusedOnlyWhileBodiesAreRead(t *testing.T) {
|
func TestAttackInAFormBodyIsRefusedOnlyWhileBodiesAreRead(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user