Compare commits

..
3 Commits
Author SHA1 Message Date
clawbot 4be0b6e7fc Rename the settings whose names do not say what they control (closes #234)
check / check (push) Waiting to run
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 08:22:52 +00:00
clawbot 101cc0aba5 Core Rule Set: a Ghost post's or page's canonical URL passes when it names a command or a file (closes #374)
check / check (push) Waiting to run
While bodies are read, 932120 refused the canonical URL
`https://example.com/blog/powershell/` and 930120 refused
`https://example.com/blog/.htaccess/`. The new exclusions 900383 and 900384
switch both rules off for `posts.N.canonical_url` under
`/ghost/api/admin/posts/` and `pages.N.canonical_url` under
`/ghost/api/admin/pages/`, as `url` does, and only when the query has no field
of that name. The recorded requests gain the saves the Ghost 6.76.0 admin
sends when those addresses are set in a post's and a page's settings.

Model: opus-5-5
2026-11-12 09:13:25 +01:00
clawbot c4bf76c966 rproxy test: send the body over its limit without Connection: close (closes #371)
check / check (push) Failing after 3m57s
visit's client sends Connection: close, and Go's server then closes the
connection after the 413 without reading the rest of the body. The
visitor, still sending, could see the reset before the answer. The test
now sends the request on its own connection without that header, so Go's
server reads the rest before it closes.

Model: opus-5-5
2026-11-12 07:44:01 +01:00
22 changed files with 171 additions and 96 deletions

No files matched your search

+11 -6
View File
@@ -1782,6 +1782,10 @@ member. A staff user's address skips them too: `username` when signing in, and
`email` when creating the first staff user, asking for a password reset or
saving a profile.
The `canonical_url` of a post or a page skips 930120 and 932120, as `url` does.
A post's skips them under `/ghost/api/admin/posts/`, and a page's under
`/ghost/api/admin/pages/`.
The articles carry HTML on purpose. These fields on the Admin API hold them:
- `lexical`, which the editor saves, and `mobiledoc` and `html`, which
@@ -1822,7 +1826,7 @@ fields, on the Admin API:
(941170 and 941210). Path traversal and SQL injection are still refused in every
field above. Script injection is too, outside the articles and code injection.
`; wget http://example.com/x.sh` is still refused in the query fields, the plain
text and the addresses.
text, the addresses and the canonical URLs.
Coraza names a staff user's `profile_image` field `json.users.0.profile_image`.
That name holds `.profile`, a file on the list of system files, so the name
@@ -2612,13 +2616,14 @@ A path under `/_sww/` that `allow` would pass on is answered `404` instead, so
that the app never sees that prefix.
`rproxy` cannot pass on an answer whose body is over 1 MiB or whose status is
below `200`. The visitor gets `502` from `rproxy`, never the default. `rproxy`
writes a process warning, its log line has no `disposition`, and it sends no
report.
below `200`. The visitor gets `502` from `rproxy`, whatever
`SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION` says. `rproxy` writes a process
warning, its log line has no `disposition`, and it sends no report.
A visitor that goes away while `waf` is being asked is no failure of `waf`.
`rproxy` closes the connection, with no warning, no default and no report. Its
log line has status `0` and no `disposition`.
`rproxy` closes the connection with no warning and no report, and
`SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION` does not apply. Its log line has
status `0` and no `disposition`.
### Passing a request to the app
+2 -2
View File
@@ -304,8 +304,8 @@ the request first.
A duration.
- `SWW_WAF_LIMIT_BAN_REPEAT_WINDOW` (default `24h`): the time after a ban ends
within which breaking a limit again lengthens the next ban. A duration.
- `SWW_WAF_LIMIT_BAN_PERMANENT_ABOVE` (default `7d`): the longest ban for a
broken limit; one that would be longer is permanent. A duration.
- `SWW_WAF_LIMIT_BAN_PERMANENT_ABOVE` (default `7d`): a ban for a broken limit
that would be longer than this is permanent instead. A duration.
- `SWW_WAF_ATTACK_BAN_DURATION` (default `7d`): the ban for a first clear sign
of attack. A duration.
- `SWW_WAF_BAN_SCOPE_V4_PREFIX` (default `32`): how much a ban on an IPv4 client
+4 -4
View File
@@ -1,6 +1,6 @@
// Package bans is the ban ledger: the bans sww makes on the
// netblocks of clients that break a rate limit, a byte limit or the error
// burst, show a clear sign of attack or are listed by the CrowdSec
// netblocks of clients that break a rate limit, a byte limit or the refusal
// limit, show a clear sign of attack or are listed by the CrowdSec
// decision list, and
// those an admin makes, with their notes, as the "Bans" section of SPEC.md
// describes. The bans are kept in memory, and written to bans.json and
@@ -49,8 +49,8 @@ type Params struct {
// CrowdSec's decision, a broken limit counts as a repeat, which bans for
// repeatFactor times as long as that ban.
LimitBanRepeatWindow time.Duration
// LimitBanPermanentAbove is the longest ban for a broken limit; one that would
// be longer is permanent instead.
// A ban for a broken limit that would be longer than
// LimitBanPermanentAbove is permanent instead.
LimitBanPermanentAbove time.Duration
// AttackBanDuration is how long a first ban for a clear sign of attack
// lasts.
+28 -1
View File
@@ -16,6 +16,7 @@ import (
"os"
"strconv"
"strings"
"time"
"github.com/spf13/viper"
)
@@ -36,7 +37,9 @@ const (
var (
errNotSize = errors.New(
"is not a size such as 512K, 100M or 5G, or off")
errNotPositive = errors.New("must be more than zero, or off")
errNotPositive = errors.New("must be more than zero, or off")
errNotMilliseconds = errors.New(
"is not a whole number of milliseconds above zero, such as 2000")
errEmptyItem = errors.New("has an empty item in its list")
errNotNetblock = errors.New(
"is not a netblock such as 10.0.0.0/8, or an address")
@@ -163,6 +166,19 @@ func (e *environment) size(name, defaultValue string) int64 {
return size
}
// milliseconds reads a setting that is a whole number of milliseconds above
// zero.
func (e *environment) milliseconds(name, defaultValue string) time.Duration {
value := e.value(name, defaultValue)
duration, valid := parseMilliseconds(value)
if !valid {
e.check(name, fmt.Errorf("%q %w", value, errNotMilliseconds))
}
return duration
}
// parseSize reads a number of bytes with an optional K, M or G suffix, in
// powers of 1024 (1K is 1024 bytes), or off.
func parseSize(value string) (int64, error) {
@@ -199,6 +215,17 @@ func splitUnit(value string) (string, int64) {
}
}
// parseMilliseconds reads a whole number of milliseconds above zero, and
// reports whether it is one.
func parseMilliseconds(value string) (time.Duration, bool) {
n, err := strconv.ParseInt(value, 10, 64)
if err != nil || n <= 0 || n > math.MaxInt64/int64(time.Millisecond) {
return 0, false
}
return time.Duration(n) * time.Millisecond, true
}
// parseList splits a comma-separated list and trims the spaces around
// each item. An empty value is an empty list.
func parseList(value string) ([]string, error) {
+1 -28
View File
@@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"log/slog"
"math"
"net/netip"
"net/url"
"strconv"
@@ -64,9 +63,7 @@ var (
"such as http://127.0.0.1:8081")
errNeedsWAFBackend = errors.New(
"is required: it is waf's URL, such as http://waf:8080")
errNotDisposition = errors.New("is not allow or deny")
errNotMilliseconds = errors.New(
"is not a whole number of milliseconds above zero, such as 2000")
errNotDisposition = errors.New("is not allow or deny")
errNotMillisecondsOrOff = errors.New(
"is not a whole number of milliseconds above zero, such as 60000, or off",
)
@@ -178,19 +175,6 @@ func (e *environment) disposition(name, defaultValue string) string {
return value
}
// milliseconds reads a setting that is a whole number of milliseconds above
// zero.
func (e *environment) milliseconds(name, defaultValue string) time.Duration {
value := e.value(name, defaultValue)
duration, valid := parseMilliseconds(value)
if !valid {
e.check(name, fmt.Errorf("%q %w", value, errNotMilliseconds))
}
return duration
}
// millisecondsOrOff reads a setting that is a whole number of milliseconds
// above zero, or off, which is 0.
func (e *environment) millisecondsOrOff(
@@ -209,17 +193,6 @@ func (e *environment) millisecondsOrOff(
return duration
}
// parseMilliseconds reads a whole number of milliseconds above zero, and
// reports whether it is one.
func parseMilliseconds(value string) (time.Duration, bool) {
n, err := strconv.ParseInt(value, 10, 64)
if err != nil || n <= 0 || n > math.MaxInt64/int64(time.Millisecond) {
return 0, false
}
return time.Duration(n) * time.Millisecond, true
}
// parseUpstreamURL reads the app's URL: http or https, a host and an
// optional port from 1 to 65535, and nothing else, since the request's
// own path and query go to the app unchanged.
+3 -2
View File
@@ -71,7 +71,8 @@ type WAF struct {
// most bytes a client's requests may carry in a minute, an hour and a
// day (SWW_WAF_BYTES_LIMIT_PER_MINUTE, SWW_WAF_BYTES_LIMIT_PER_HOUR and
// SWW_WAF_BYTES_LIMIT_PER_DAY). BytesCounted is which body bytes count
// toward them (SWW_WAF_BYTES_COUNTED): response, request or both.
// toward them and the byte anomaly thresholds (SWW_WAF_BYTES_COUNTED):
// response, request or both.
BytesLimitPerMinute int64
BytesLimitPerHour int64
BytesLimitPerDay int64
@@ -846,7 +847,7 @@ func (e *environment) count(name, defaultValue string) int64 {
}
// bytesCounted reads the setting that is which body bytes count toward the
// byte limits: response, request or both.
// byte limits and the byte anomaly thresholds: response, request or both.
func (e *environment) bytesCounted(name, defaultValue string) string {
value := e.value(name, defaultValue)
if value != "response" && value != "request" && value != "both" {
+1 -1
View File
@@ -2942,7 +2942,7 @@ func TestCorazaWritesNoFile(t *testing.T) {
wantBody(t, crs, post(multipart), body, matched(942100), body)
}
func TestBodyLimitOf1GLoads(t *testing.T) {
func TestBodyInspectionSizeOf1GLoads(t *testing.T) {
t.Parallel()
_, err := coreruleset.New(
+17
View File
@@ -216,6 +216,23 @@ SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/" \
SecRule &ARGS_GET:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html|codeinjection_(?:head|foot))|settings\.\d+\.value)$/ "@eq 0" \
"ctl:ruleRemoveTargetById=941110;ARGS:/^json\.(?:(?:posts|pages)\.\d+\.(?:lexical|mobiledoc|html|codeinjection_(?:head|foot))|settings\.\d+\.value)$/"
# The canonical URL of a post or a page skips the two rules that url skips:
# 930120: https://example.com/blog/.htaccess/
# 932120: https://example.com/blog/powershell/
# Each of these fields skips them only on its own path:
# posts.<n>.canonical_url, under /ghost/api/admin/posts/
# pages.<n>.canonical_url, under /ghost/api/admin/pages/
SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/posts/" \
"id:900383,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^json\.posts\.\d+\.canonical_url$/ "@eq 0" \
"ctl:ruleRemoveTargetById=930120;ARGS:/^json\.posts\.\d+\.canonical_url$/,\
ctl:ruleRemoveTargetById=932120;ARGS:/^json\.posts\.\d+\.canonical_url$/"
SecRule REQUEST_FILENAME "@beginsWith /ghost/api/admin/pages/" \
"id:900384,phase:1,pass,nolog,chain"
SecRule &ARGS_GET:/^json\.pages\.\d+\.canonical_url$/ "@eq 0" \
"ctl:ruleRemoveTargetById=930120;ARGS:/^json\.pages\.\d+\.canonical_url$/,\
ctl:ruleRemoveTargetById=932120;ARGS:/^json\.pages\.\d+\.canonical_url$/"
# Saving a staff user's profile sends the field profile_image, whose name holds
# .profile, a file the list of system files names (930120).
SecRule REQUEST_FILENAME "@rx ^/ghost/api/admin/users/[0-9a-f]{24}/$" \
+32 -5
View File
@@ -143,6 +143,16 @@ var ghostAddressFields = []ghostField{
{http.MethodPut, ghostUser, `{"users":[{"email":"VALUE"}]}`},
}
// ghostCanonicalURLFields hold the canonical URL of a post or a page, and skip
// 930120 and 932120, as url does.
//
//nolint:gochecknoglobals // a constant cannot be a list
var ghostCanonicalURLFields = []ghostField{
{http.MethodPut, ghostPost, `{"posts":[{"canonical_url":"VALUE"}]}`},
{http.MethodPost, ghostPosts, `{"posts":[{"canonical_url":"VALUE"}]}`},
{http.MethodPut, ghostPage, `{"pages":[{"canonical_url":"VALUE"}]}`},
}
// ghostPostFields hold a post or a page, skip the rules of 900355, and take a
// <script> element by 900366.
//
@@ -220,13 +230,16 @@ var ghostQueryFields = []string{
ghostAdminFilter, ghostContentFilter, ghostSearch, ghostEmbed,
}
// ghostURLs are addresses 930120 and 932120 refused in url.
// ghostURLs are addresses 930120 and 932120 refused in url and in a canonical
// URL.
//
//nolint:gochecknoglobals // a constant cannot be a list
var ghostURLs = []string{
"https://github.com/example/site/blob/main/.htaccess",
"https://example.com/blog/powershell-tips?ref=ghost",
"https://example.com/docs/python3/",
"https://example.com/blog/powershell/",
"https://example.com/blog/.htaccess/",
}
// ghostAddresses are addresses 930120, 932120 and 932260 refused.
@@ -327,6 +340,10 @@ func TestGhostOrdinaryValuesPassInTheExcludedFields(t *testing.T) {
for _, address := range ghostURLs {
wantGhostQuery(t, ghostEmbed+url.QueryEscape(address), false)
for _, f := range ghostCanonicalURLFields {
wantGhostField(t, f, address, false)
}
}
for _, address := range ghostAddresses {
@@ -395,7 +412,7 @@ func TestGhostAttacksAreRefusedInTheExcludedFields(t *testing.T) {
for _, attack := range ghostAttacks {
for _, f := range slices.Concat(ghostTextFields, ghostAddressFields,
ghostArticleFields, ghostCodeFields) {
ghostCanonicalURLFields, ghostArticleFields, ghostCodeFields) {
wantGhostField(t, f, attack, true)
}
@@ -407,7 +424,7 @@ func TestGhostAttacksAreRefusedInTheExcludedFields(t *testing.T) {
// A script injection stays refused in every field that does not take a
// <script> element.
for _, f := range slices.Concat(ghostTextFields, ghostAddressFields,
ghostSnippetAndNewsletterFooterFields) {
ghostCanonicalURLFields, ghostSnippetAndNewsletterFooterFields) {
wantGhostField(t, f, scriptTag, true)
}
@@ -416,8 +433,10 @@ func TestGhostAttacksAreRefusedInTheExcludedFields(t *testing.T) {
}
// A command after a separator stays refused wherever its rules do not
// misfire: in the query fields, the plain text and the addresses.
for _, f := range slices.Concat(ghostTextFields, ghostAddressFields) {
// misfire: in the query fields, the plain text, the addresses and the
// canonical URLs.
for _, f := range slices.Concat(ghostTextFields, ghostAddressFields,
ghostCanonicalURLFields) {
wantGhostField(t, f, commandAfterASeparator, true)
}
@@ -462,6 +481,8 @@ func TestGhostExclusionsAreNarrow(t *testing.T) {
setupEmail = `{"setup":[{"email":"python3.dev@example.com"}]}`
passwordResetEmail = `{"password_reset":[{"email":"python3.dev@example.com"}]}`
userEmail = `{"users":[{"email":"python3.dev@example.com"}]}`
postURL = `{"posts":[{"canonical_url":"https://example.com/powershell/"}]}`
pageURL = `{"pages":[{"canonical_url":"https://example.com/powershell/"}]}`
)
for _, refused := range []struct{ target, body string }{
@@ -498,6 +519,12 @@ func TestGhostExclusionsAreNarrow(t *testing.T) {
{ghostPasswordReset + "?json.password_reset.0.email=x", passwordResetEmail},
{ghostPosts, userEmail},
{ghostUser + "?json.users.0.email=x", userEmail},
{"/api/posts/1/", postURL},
{ghostMembers, postURL},
{ghostPage, postURL},
{ghostPost + "?json.posts.0.canonical_url=x", postURL},
{ghostPost, pageURL},
{ghostPage + "?json.pages.0.canonical_url=x", pageURL},
{ghostSession, `{"username":"x","pass":"' OR '1'='1"}`},
{"/ghost/api/admin/users/me/", `{"password":"' OR '1'='1"}`},
{"/ghost/api/admin/posts/1/", `{"users":[{"profile_image":null}]}`},
+2
View File
@@ -533,6 +533,8 @@
{"method": "GET", "target": "/ghost/api/admin/pages/6af45d34d281b00001a599a0/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "PUT", "target": "/ghost/api/admin/pages/6af45d34d281b00001a599a0/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 3980", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"pages\":[{\"title\":\"About: python3, powershell & .htaccess\",\"slug\":\"about-python3-powershell-htaccess\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Who writes here, and why.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"About this site\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Notes on \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"python3\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\", \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":2,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" and the shell. Start with \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la ~/.ssh\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" and read \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess primer\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"https://example.com/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\".\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"Get-ChildItem -Force | Where-Object { $_.Name -like '.htaccess' }\\\\nInvoke-WebRequest -Uri https://example.com/feed.xml -OutFile feed.xml\\\",\\\"language\\\":\\\"powershell\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Contact\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Write to \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"alice@example.com\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"mailto:alice@example.com\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\".\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\Line truncated
{"method": "PUT", "target": "/ghost/api/admin/pages/6af45d34d281b00001a599a0/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 3984", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": "{\"pages\":[{\"title\":\"About: python3, powershell & .htaccess\",\"slug\":\"about-python3-powershell-htaccess\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Who writes here, and why.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"About this site\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h2\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Notes on \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":1,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"python3\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\", \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":2,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"powershell\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" and the shell. Start with \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":16,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"ls -la ~/.ssh\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\" and read \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"the .htaccess primer\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"https://example.com/htaccess\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\".\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\\"type\\\":\\\"codeblock\\\",\\\"version\\\":1,\\\"code\\\":\\\"Get-ChildItem -Force | Where-Object { $_.Name -like '.htaccess' }\\\\nInvoke-WebRequest -Uri https://example.com/feed.xml -OutFile feed.xml\\\",\\\"language\\\":\\\"powershell\\\",\\\"caption\\\":\\\"\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Contact\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"extended-heading\\\",\\\"version\\\":1,\\\"tag\\\":\\\"h3\\\"},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"Write to \\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1},{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"alice@example.com\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"link\\\",\\\"version\\\":1,\\\"rel\\\":null,\\\"target\\\":null,\\\"title\\\":null,\\\"url\\\":\\\"mailto:alice@example.com\\\"},{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\".\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1},{\\Line truncated
{"method": "PUT", "target": "/ghost/api/admin/posts/6af5659a6dde5d0001d3d72e/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 713", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3AoIVlIxucaQas8uV_V0tI2mNyxYEoOjBC.ajmnN2F%2BNe0IFunZWICXaJjQpUSV75M6lrkP5I0svVs"], "body": "{\"posts\":[{\"title\":\"Getting started with PowerShell\",\"slug\":\"getting-started-with-powershell\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"First published on the old blog.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"root\\\",\\\"version\\\":1}}\",\"feature_image\":null,\"feature_image_alt\":null,\"feature_image_caption\":null,\"status\":\"draft\",\"published_at\":null,\"canonical_url\":\"https://example.com/blog/powershell/\",\"id\":\"6af5659a6dde5d0001d3d72e\",\"updated_at\":\"2026-11-12T07:07:12.000Z\"}]}"}
{"method": "PUT", "target": "/ghost/api/admin/pages/6af565b96dde5d0001d3d735/?formats=mobiledoc%2Clexical&include=tags%2Cauthors%2Cauthors.roles%2Cemail%2Ctiers%2Cnewsletter%2Ccount.clicks%2Cpost_revisions%2Cpost_revisions.author", "headers": ["Host: example.com", "Content-Length: 701", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "content-type: application/json", "app-pragma: no-cache", "Accept: */*", "Origin: http://example.com", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3AoIVlIxucaQas8uV_V0tI2mNyxYEoOjBC.ajmnN2F%2BNe0IFunZWICXaJjQpUSV75M6lrkP5I0svVs"], "body": "{\"pages\":[{\"title\":\"Rewrite rules in .htaccess\",\"slug\":\"rewrite-rules-in-htaccess\",\"lexical\":\"{\\\"root\\\":{\\\"children\\\":[{\\\"children\\\":[{\\\"detail\\\":0,\\\"format\\\":0,\\\"mode\\\":\\\"normal\\\",\\\"style\\\":\\\"\\\",\\\"text\\\":\\\"First published on the old blog.\\\",\\\"type\\\":\\\"extended-text\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"paragraph\\\",\\\"version\\\":1}],\\\"direction\\\":\\\"ltr\\\",\\\"format\\\":\\\"\\\",\\\"indent\\\":0,\\\"type\\\":\\\"root\\\",\\\"version\\\":1}}\",\"feature_image\":null,\"feature_image_alt\":null,\"feature_image_caption\":null,\"status\":\"draft\",\"published_at\":null,\"canonical_url\":\"https://example.com/blog/.htaccess/\",\"id\":\"6af565b96dde5d0001d3d735\",\"updated_at\":\"2026-11-12T07:07:42.000Z\"}]}"}
{"method": "GET", "target": "/ghost/assets/pages-route-Bx4g0rd3.js", "headers": ["Host: example.com", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "Origin: http://example.com", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/pages/?filter=status%3Ascheduled&order=published_at+desc&limit=30", "headers": ["Host: example.com", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
{"method": "GET", "target": "/ghost/api/admin/pages/?filter=status%3Adraft&order=updated_at+desc&limit=30", "headers": ["Host: example.com", "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.8078.4 Safari/537.36", "app-pragma: no-cache", "Accept: */*", "Referer: http://example.com/ghost/", "Accept-Encoding: gzip, deflate", "Cookie: ghost-admin-api-session=s%3ApyQuEYuuhAK8u18lU0bDl1kAjTTCtLBe.fHhgUS7MTBvPYpROnyCJjqXG%2BzBuicl%2Bw6WGQi%2Brq5Q"], "body": ""}
+2 -2
View File
@@ -185,8 +185,8 @@ func New(limits Limits, maxClients int) *Limiter {
}
}
// Hit is a request that takes a client over a rate limit or the error
// burst, or whose bytes take it over a byte limit.
// Hit is a request that takes a client over a rate limit or the refusal
// limit, or whose bytes take it over a byte limit.
type Hit struct {
// Kind is KindRequests for a rate limit, KindBytes for a byte limit,
// KindRefusals for the refusal limit.
+3 -1
View File
@@ -351,7 +351,9 @@ func TestResetSetsTheBytesBackToZero(t *testing.T) {
wantBytesCount(t, limiter, client, start, 1000, "")
}
func TestRefusalsOverTheThresholdInAMinuteBreakTheErrorBurst(t *testing.T) {
func TestRefusalsOverTheThresholdInAMinuteBreakTheRefusalLimit(
t *testing.T,
) {
t.Parallel()
limiter := ratelimit.New(ratelimit.Limits{}, tableSize)
+9 -4
View File
@@ -93,10 +93,15 @@ func TestABodyAnnouncedOverItsLimitIsRefusedOnceWAFAllowsIt(t *testing.T) {
},
)
got := visit(t, newRequest(t, http.MethodPost, addr, "/",
bytes.NewReader(make([]byte, tc.size))))
if got.status != http.StatusRequestEntityTooLarge {
t.Errorf("answered %+v, want 413", got)
// The visitor sends no Connection: close, so Go's server reads the rest
// of the body before it closes the connection. Closed with the rest
// unread, the connection is reset while the visitor still sends.
conn := dial(t, addr)
send(t, conn, post+announcedLength(tc.size)+
strings.Repeat("x", tc.size))
if status := readStatus(t, conn); status != http.StatusRequestEntityTooLarge {
t.Errorf("status %d, want 413", status)
}
if asked := standIn.question(t); len(asked.body) != tc.asked {
+23 -10
View File
@@ -956,7 +956,8 @@ func answeringWith(answer http.HandlerFunc) func(t *testing.T) string {
}
}
// noAnswers are the ways waf can leave the decision to the default.
// noAnswers are the ways waf can leave the decision to
// SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION.
func noAnswers() map[string]func(t *testing.T) string {
return map[string]func(t *testing.T) string{
"no connection": func(*testing.T) string { return nowhere },
@@ -1008,7 +1009,8 @@ func TestAnAnswerOtherThanAllowGoesToTheVisitorAsWAFGaveIt(t *testing.T) {
{"an unknown disposition", "maybe", "who knows\n", http.StatusOK},
} {
for _, setting := range []string{dispositionAllow, dispositionDeny} {
t.Run(tc.name+" with the default "+setting, func(t *testing.T) {
subtest := tc.name + " with " + noAnswerSetting + "=" + setting
t.Run(subtest, func(t *testing.T) {
t.Parallel()
standIn := startWAF(
@@ -1049,7 +1051,7 @@ func TestAnAnswerOtherThanAllowGoesToTheVisitorAsWAFGaveIt(t *testing.T) {
if line["status"] != float64(tc.status) ||
line["waf_backend_no_answer"] != nil {
t.Errorf(
"line %v, want status %d and no default",
"line %v, want status %d and no waf_backend_no_answer",
line,
tc.status,
)
@@ -1202,7 +1204,8 @@ func TestAnAnswerThatCannotBePassedOnIsAnswered502(t *testing.T) {
"a 101 that holds its connection open": answeringWith(switchAndHold),
} {
for _, setting := range []string{dispositionAllow, dispositionDeny} {
t.Run(name+" with the default "+setting, func(t *testing.T) {
subtest := name + " with " + noAnswerSetting + "=" + setting
t.Run(subtest, func(t *testing.T) {
t.Parallel()
app := startApp(t, func(http.ResponseWriter, *http.Request) {
@@ -1253,7 +1256,9 @@ func TestAnAnswerThatCannotBePassedOnGetsNoReport(t *testing.T) {
standIn.wantNoReport(t)
}
func TestWithoutAnAnswerTheDefaultDispositionDecides(t *testing.T) {
func TestWithoutAnAnswerTheWAFBackendNoAnswerDispositionDecides(
t *testing.T,
) {
t.Parallel()
for name, backend := range noAnswers() {
@@ -1275,7 +1280,10 @@ func TestWithoutAnAnswerTheDefaultDispositionDecides(t *testing.T) {
line := out.requestLine(t)
if line["disposition"] != dispositionAllow ||
line["waf_backend_no_answer"] != true {
t.Errorf("line %v, want allow by default", line)
t.Errorf(
"line %v, want allow and waf_backend_no_answer",
line,
)
}
})
@@ -1303,17 +1311,20 @@ func TestWithoutAnAnswerTheDefaultDispositionDecides(t *testing.T) {
line := out.requestLine(t)
if line["disposition"] != dispositionDeny ||
line["waf_backend_no_answer"] != true {
t.Errorf("line %v, want deny by default", line)
t.Errorf(
"line %v, want deny and waf_backend_no_answer",
line,
)
}
})
}
}
func TestAVisitorThatGoesAwayWhileWAFIsAskedGetsNoDefault(t *testing.T) {
func TestAVisitorThatGoesAwayWhileWAFIsAskedGetsNoDisposition(t *testing.T) {
t.Parallel()
for _, disposition := range []string{dispositionAllow, dispositionDeny} {
t.Run("with the default "+disposition, func(t *testing.T) {
t.Run(noAnswerSetting+"="+disposition, func(t *testing.T) {
t.Parallel()
standIn := startWAF(
@@ -1356,7 +1367,9 @@ func TestAVisitorThatGoesAwayWhileWAFIsAskedGetsNoDefault(t *testing.T) {
}
}
func TestRProxyWarnsWhenTheDefaultDecides(t *testing.T) {
func TestRProxyWarnsWhenTheWAFBackendNoAnswerDispositionDecides(
t *testing.T,
) {
t.Parallel()
app := startApp(t, greet)
+2 -1
View File
@@ -241,7 +241,8 @@ func TestTheSiteAnswersWhileWAFIsStoppedAndStartedAgain(t *testing.T) {
forwardTo("")
stop()
// With waf down, the site answers, and rproxy says the default did it.
// With waf down, the site answers, and rproxy says
// SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION did it.
wantStatus(t, site+"/after-stop", placed, http.StatusOK)
line := out.line(t, "path", "/after-stop")
+1 -1
View File
@@ -602,7 +602,7 @@ func TestRuleFileThatDoesNotParseStopsTheStart(t *testing.T) {
)
}
func TestRulesDirThatDoesNotExistStopsTheStart(t *testing.T) {
func TestRuleFilesDirThatDoesNotExistStopsTheStart(t *testing.T) {
t.Parallel()
dir := filepath.Join(t.TempDir(), "rules.d")
+5 -3
View File
@@ -150,7 +150,7 @@ func TestEachBiasedThresholdLowersTheByteLimits(t *testing.T) {
}
}
func TestBytesPercentSettingsTakeThePlaceOfTheOthersForByteLimits(
func TestBytesLimitPercentSettingsTakeThePlaceOfTheOthersForByteLimits(
t *testing.T,
) {
t.Parallel()
@@ -303,7 +303,7 @@ func TestLowestPercentageApplies(t *testing.T) {
}
}
func TestUnknownLimitPercentGivesEveryClientWithoutACountryItsPercentage(
func TestUnknownCountryLimitPercentGivesEveryClientWithoutACountryItsPercentage(
t *testing.T,
) {
t.Parallel()
@@ -330,7 +330,9 @@ func TestUnknownLimitPercentGivesEveryClientWithoutACountryItsPercentage(
}
}
func TestClientWithoutAnAnswerInTimeHasTheUnknownLimitPercent(t *testing.T) {
func TestClientWithoutAnAnswerInTimeHasTheUnknownCountryLimitPercent(
t *testing.T,
) {
t.Parallel()
// In a synctest bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays
+1 -1
View File
@@ -239,7 +239,7 @@ func TestWebSocketBytesAreCountedOnceItCloses(t *testing.T) {
}
}
func TestBytesCountSaysWhichBytesCount(t *testing.T) {
func TestBytesCountedSaysWhichBytesAreCounted(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
+3 -3
View File
@@ -276,7 +276,7 @@ func TestCoreRuleSetIsNotRunWhenOffOrForAnExemptClientPathOrRuleFileRefusal(
wantCoreRuleSet(t, line, nil)
}
func TestAnomalyThreshold(t *testing.T) {
func TestCoreRuleSetScoreThreshold(t *testing.T) {
t.Parallel()
// A score under the threshold, or with the threshold off, is logged,
@@ -526,7 +526,7 @@ func TestResponsesAreNotInspected(t *testing.T) {
wantLine(t, out.endedLine(t), http.StatusOK, requestlog.ActionForward)
}
func TestCoreRuleSetRefusalIsAnOffenceAndCountsTowardTheErrorBurst(
func TestCoreRuleSetRefusalIsAnOffenceAndCountsTowardTheRefusalLimit(
t *testing.T,
) {
t.Parallel()
@@ -582,7 +582,7 @@ func TestCoreRuleSetRefusalIsAnOffenceAndCountsTowardTheErrorBurst(
)
}
func TestDetectModeMatchIsNoOffenceAndNotCountedTowardTheErrorBurst(
func TestDetectModeMatchIsNoOffenceAndNotCountedTowardTheRefusalLimit(
t *testing.T,
) {
t.Parallel()
+17 -17
View File
@@ -48,9 +48,9 @@ func (r refused) send(s *sender, from string) logLine {
return logLine{}
}
// startForErrorBurst is startWithClock with testRules, the metrics token and
// startForRefusalLimit is startWithClock with testRules, the metrics token and
// SWW_WAF_REFUSALS_LIMIT_PER_MINUTE at threshold, and the settings in env.
func startForErrorBurst(
func startForRefusalLimit(
t *testing.T, threshold string, env map[string]string,
) (*sender, *clock, *waf.Server) {
t.Helper()
@@ -65,7 +65,7 @@ func startForErrorBurst(
return startWithClock(t, "", settings)
}
func TestErrorBurstBreaksAtOneOverTheThreshold(t *testing.T) {
func TestRefusalLimitBreaksAtOneOverTheThreshold(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
@@ -90,7 +90,7 @@ func TestErrorBurstBreaksAtOneOverTheThreshold(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
s, _, _ := startForErrorBurst(t, "3", nil)
s, _, _ := startForRefusalLimit(t, "3", nil)
// Three refusals break nothing, and the app's answers between
// them are not counted.
@@ -109,8 +109,8 @@ func TestErrorBurstBreaksAtOneOverTheThreshold(t *testing.T) {
s.get(client, http.StatusOK, requestlog.ActionForward)
}
// The fourth is answered as the others were, breaks the error
// burst, and bans the client.
// The fourth is answered as the others were, breaks the refusal
// limit, and bans the client.
line := tc.refusals[3].send(s, client)
if line.LimitHit != requestlog.LimitHitMinuteRefusals ||
line.Offence != requestlog.OffenceLimit {
@@ -130,7 +130,7 @@ func TestErrorBurstBreaksAtOneOverTheThreshold(t *testing.T) {
func TestRefusalLimitBanNamesItsHeuristicWindowCountAndLimit(t *testing.T) {
t.Parallel()
s, _, _ := startForErrorBurst(t, "1", nil)
s, _, _ := startForRefusalLimit(t, "1", nil)
const ruleFigures = `{"rule_id": "blocked", "rule_action": "block"}`
@@ -146,10 +146,10 @@ func TestRefusalLimitBanNamesItsHeuristicWindowCountAndLimit(t *testing.T) {
`{"window": "minute", "count": 2, "limit": 1}`)
}
func TestErrorBurstBanNotesHistoryAndMetrics(t *testing.T) {
func TestRefusalLimitBanNotesHistoryAndMetrics(t *testing.T) {
t.Parallel()
s, clk, server := startForErrorBurst(t, "2", nil)
s, clk, server := startForRefusalLimit(t, "2", nil)
start := clk.Now()
blockRule.send(s, client)
@@ -221,7 +221,7 @@ func TestErrorBurstBanNotesHistoryAndMetrics(t *testing.T) {
)
}
func TestErrorBurstIsNotLoweredForAClientWithLowerLimits(t *testing.T) {
func TestRefusalLimitIsNotLoweredForAClientWithLowerLimits(t *testing.T) {
t.Parallel()
geojsURL, _ := startGeoJS(t)
@@ -252,7 +252,7 @@ func TestErrorBurstIsNotLoweredForAClientWithLowerLimits(t *testing.T) {
}
}
func TestErrorBurstDoesNotCountTheAppsAnswers(t *testing.T) {
func TestRefusalLimitDoesNotCountTheAppsAnswers(t *testing.T) {
t.Parallel()
statuses := map[string]int{
@@ -293,7 +293,7 @@ func TestErrorBurstDoesNotCountTheAppsAnswers(t *testing.T) {
wantAlerts(t, queue)
}
func TestErrorBurstOffOrAtItsDefault(t *testing.T) {
func TestRefusalLimitOffOrAtItsDefault(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
@@ -331,7 +331,7 @@ func TestErrorBurstOffOrAtItsDefault(t *testing.T) {
}
}
func TestErrorBurstCountsEachClientTheChecksApplyTo(t *testing.T) {
func TestRefusalLimitCountsEachClientTheChecksApplyTo(t *testing.T) {
t.Parallel()
const (
@@ -339,7 +339,7 @@ func TestErrorBurstCountsEachClientTheChecksApplyTo(t *testing.T) {
exempt = "192.0.2.50" // in SWW_WAF_RATE_LIMIT_EXEMPT_NETS
)
s, _, _ := startForErrorBurst(t, "1", map[string]string{
s, _, _ := startForRefusalLimit(t, "1", map[string]string{
allowNets: allowed, rateLimitExemptNets: exempt,
})
@@ -367,10 +367,10 @@ func TestErrorBurstCountsEachClientTheChecksApplyTo(t *testing.T) {
s.get(exempt, http.StatusForbidden, requestlog.ActionTemporarilyBanned)
}
func TestErrorBurstBanSetsTheRefusalsBackToZero(t *testing.T) {
func TestRefusalLimitBanSetsTheRefusalsBackToZero(t *testing.T) {
t.Parallel()
s, clk, _ := startForErrorBurst(
s, clk, _ := startForRefusalLimit(
t,
"1",
map[string]string{limitBanDuration: "1s"},
@@ -389,7 +389,7 @@ func TestErrorBurstBanSetsTheRefusalsBackToZero(t *testing.T) {
}
}
func TestObserveModeLogsAndAlertsTheErrorBurst(t *testing.T) {
func TestObserveModeLogsAndAlertsABrokenRefusalLimit(t *testing.T) {
t.Parallel()
s, clk, server, queue := startWithAlerts(t, map[string]string{
+2 -2
View File
@@ -432,7 +432,7 @@ const (
noResolver = "127.0.0.1:9"
)
func TestEachReputationActionForAClientADNSBLZoneLists(t *testing.T) {
func TestEachDNSBLAbuseIPDBActionForAClientADNSBLZoneLists(t *testing.T) {
t.Parallel()
forward, denied := requestlog.ActionForward, requestlog.ActionClientDenied
@@ -989,7 +989,7 @@ func TestClientRefusedForAnOffenceIsCheckedWithAbuseIPDBAtItsNextRequest(
}
}
func TestEachReputationActionForAClientAbuseIPDBScoresAtOrOverTheMinimum(
func TestEachDNSBLAbuseIPDBActionForAClientAbuseIPDBScoresAtOrOverTheMinimum(
t *testing.T,
) {
t.Parallel()
+2 -2
View File
@@ -811,7 +811,7 @@ func TestObserveModeAllowsAndLogsWhatEnforceModeWouldHaveDone(t *testing.T) {
wantWouldAction(t, line, requestlog.ActionClientDenied)
}
func TestObserveModeRefusalAnsweredAllowCountsForTheErrorBurst(
func TestObserveModeRefusalAnsweredAllowCountsForTheRefusalLimit(
t *testing.T,
) {
t.Parallel()
@@ -837,7 +837,7 @@ func TestObserveModeRefusalAnsweredAllowCountsForTheErrorBurst(
}
}
func TestDeniedQuestionCountsForTheErrorBurst(t *testing.T) {
func TestDeniedQuestionCountsForTheRefusalLimit(t *testing.T) {
t.Parallel()
s := startWAF(t, "", map[string]string{