Compare commits

..
Author SHA1 Message Date
clawbot 7caad07de3 Core Rule Set: the API searches gitea and Forgejo share pass (closes #353)
check / check (push) Successful in 4m34s
Recorded with python3, powershell, ls -la, .htaccess, file names and plain
words from gitea 1.27.3 and Forgejo 17.0.1. q on the API's searches of
topics, of an organisation's teams and of emails joins 900603, which skips
930120, 932120, 932250 and 932260. The same four rules are skipped by
query on the search of unadopted repositories (900607) and by name on the
search of a repository's milestones (900608). 900605 also lets /bin/sh
through in q on the API's search of packages, since a package name can
hold a slash. The searches of repositories, users and issues already
passed.

Model: opus-5-5
2026-11-12 10:54:35 +00:00
14 changed files with 61 additions and 697 deletions

No files matched your search

+3 -4
View File
@@ -543,10 +543,9 @@ request reaches the app.
A refused visitor is answered with `SWW_WAF_BAN_RESPONSE`. A `block` rule or a A refused visitor is answered with `SWW_WAF_BAN_RESPONSE`. A `block` rule or a
Core Rule Set match answers `403` and bans nobody, since the Core Rule Set has Core Rule Set match answers `403` and bans nobody, since the Core Rule Set has
false positives; such refusals count toward `SWW_WAF_REFUSALS_LIMIT_PER_MINUTE` false positives; such refusals count toward `SWW_WAF_REFUSALS_LIMIT_PER_MINUTE`
instead. A request over the header limits of step 3 is answered `414` for a instead. A request over the header limits of step 3 is answered `431`, and bans
request line over 8 KiB and `431` otherwise. It bans nobody either. `rproxy` nobody either. `rproxy` itself answers `405` to a method it does not pass on,
itself answers `405` to a method it does not pass on, and the status for a size and the status for a size or time limit, such as `413` for a body too large.
or time limit, such as `413` for a body too large.
### Clients and bans ### Clients and bans
+21 -58
View File
@@ -151,11 +151,11 @@ field gives that name. `refusal_limit` and `byte_limits` give theirs in
around its address, a /64 by default, since one abuser usually controls a around its address, a /64 by default, since one abuser usually controls a
whole /64. A client is therefore one IPv4 address or one IPv6 group. whole /64. A client is therefore one IPv4 address or one IPv6 group.
3. The header limits (`header_limits`; see "Answering in time"). They come first 3. The header limits (`header_limits`; see "Answering in time"). They come first
after `SWW_WAF_ALLOW_NETS`, since they only measure lines and count header after `SWW_WAF_ALLOW_NETS`, since they only count header fields and cookies.
fields and cookies. Next, the tag lists (`tag_lists`; see "Request tags"). Next, the tag lists (`tag_lists`; see "Request tags"). Each list that holds
Each list that holds the client adds its tag. Next, `routewatch_tags` adds the client adds its tag. Next, `routewatch_tags` adds the client's `asn=` and
the client's `asn=` and `prefix=` tags while `SWW_WAF_ROUTEWATCH_URL` is set. `prefix=` tags while `SWW_WAF_ROUTEWATCH_URL` is set. A client in
A client in `SWW_WAF_ALLOW_NETS` skips the header limits and gets no tag. `SWW_WAF_ALLOW_NETS` skips the header limits and gets no tag.
4. The static lists. A client in `SWW_WAF_ALLOW_NETS` skips steps 5 to 15, the 4. The static lists. A client in `SWW_WAF_ALLOW_NETS` skips steps 5 to 15, the
byte limits and the refusal limit. A client in `SWW_WAF_DENY_NETS` is refused byte limits and the refusal limit. A client in `SWW_WAF_DENY_NETS` is refused
(`deny_nets`); a client in both lists is let through. (`deny_nets`); a client in both lists is let through.
@@ -253,9 +253,9 @@ does not reach neighbours who did nothing. Requests refused under a ban are not
counted against limits. They are counted in the client's history and in the counted against limits. They are counted in the client's history and in the
ban's notes. ban's notes.
A request over the header limits is refused with `414` or `431` before the bans A request over the header limits is refused with `431` before the bans are
are checked (see "Answering in time"). It makes no ban, makes no ban permanent checked (see "Answering in time"). It makes no ban, makes no ban permanent and
and is not counted in a ban's notes. is not counted in a ban's notes.
An offence is a request `waf` holds against the client. It is one of: An offence is a request `waf` holds against the client. It is one of:
@@ -888,8 +888,7 @@ one entry of `apps` in `internal/coreruleset/coreruleset.go`. gitea's fixes are:
- a repository's branches, tags and packages pages; - a repository's branches, tags and packages pages;
- an owner's packages page; - an owner's packages page;
- the admin page of packages; - the admin page of packages;
- the API's search of packages; - the API's search of packages.
- the searches of the Composer and Conan registries.
- `q` on gitea's other searches, for the same four rules. Words such as - `q` on gitea's other searches, for the same four rules. Words such as
`python3`, `powershell`, `ls -la` and `.htaccess` were refused there. The `python3`, `powershell`, `ls -la` and `.htaccess` were refused there. The
@@ -906,9 +905,7 @@ one entry of `apps` in `internal/coreruleset/coreruleset.go`. gitea's fixes are:
- the admin lists of users, organisations, repositories, unadopted - the admin lists of users, organisations, repositories, unadopted
repositories and emails; repositories and emails;
- the API's searches of topics, of an organisation's teams and of - the API's searches of topics, of an organisation's teams and of
emails; emails.
- the searches of the Cargo, Chef, Composer and Conan registries, and
the NuGet registry's search service.
Most send `q` in the query string. The admin forms that adopt a Most send `q` in the query string. The admin forms that adopt a
repository or activate an email send it in the body. gitea serves its repository or activate an email send it in the body. gitea serves its
@@ -919,31 +916,6 @@ one entry of `apps` in `internal/coreruleset/coreruleset.go`. gitea's fixes are:
and a name, such as `alice/python3-app`; and a name, such as `alice/python3-app`;
- `name` of the search of a repository's milestones. - `name` of the search of a repository's milestones.
- Fields of the package registries' searches:
- `text` of the npm registry's search, for the same four rules and the
rule for shell paths (932160). An npm name can hold a slash, as
`@alice/tools-cli` does.
- `searchTerm` of the NuGet v2 API's searches, for the same four rules.
These are the searches:
- `Search()` and `Packages()`;
- `Search()/$count` and `Packages()/$count`, which count their
results.
- `$filter` of those searches, for the lists of system files and Windows
commands (930120, 932120). It holds an expression such as
`tolower(Id) eq 'python3'`.
- The names `$filter`, `$orderby` and `$top` on those searches, for the
rule for MongoDB operators (942290). It refused every such search.
- `q` of a Conan search, for the rule for SQL injection (942100), which
refused the pattern `zlib/*`. `q` skips it only when the request has
one `q`, in the query string or the body. That `q` has the shape of a
Conan reference. A reference is a name, then optionally `/` and a
version, then optionally `@` and a user, then optionally `/` and a
channel. Each part is made of letters, digits, `_`, `.`, `-` and `*`.
A part holds no `+`, since 942100 reads `+` as a space. `q` also holds
no `*/`, which ends an SQL comment, and no `--`, which starts one. And
942100 finds no SQL injection in `q` once each comment that `/*`
starts, an unclosed one included, is replaced by a space.
- The path of these requests, which names a file: - The path of these requests, which names a file:
- a file's page; - a file's page;
- its raw file; - its raw file;
@@ -2403,7 +2375,7 @@ answer but the last below carries `X-Sww-Disposition`.
of: of:
- `SWW_WAF_BAN_RESPONSE`, for a refusal; - `SWW_WAF_BAN_RESPONSE`, for a refusal;
- `403`, for a `block` rule or the Core Rule Set; - `403`, for a `block` rule or the Core Rule Set;
- `414` or `431`, for a request over the header limits; - `431`, for a request over the header limits;
- `404`, for a path under `/_sww/` that passes the checks, so that the app - `404`, for a path under `/_sww/` that passes the checks, so that the app
never sees that prefix. never sees that prefix.
- `X-Sww-Disposition: close`, status `200`, no body. This takes the place of a - `X-Sww-Disposition: close`, status `200`, no body. This takes the place of a
@@ -2465,31 +2437,22 @@ never wait. These can still wait for GeoJS:
- a rule file; - a rule file;
- the Core Rule Set. - the Core Rule Set.
The header limits are fixed, with no setting. A line's length is in bytes and The header limits are fixed, with no setting. `waf` refuses a request over
does not count its line ending. `waf` refuses a request whose request line is either one:
longer than 8 KiB with `414`. An example is `GET /search?q=` followed by 30,000
characters.
It refuses a request with `431` for any of:
- a header line longer than 8 KiB. Each value of a header sent twice is a line
of its own. `Host` counts as a line.
- more than 50 cookies. Each part of its `Cookie` headers between semicolons - more than 50 cookies. Each part of its `Cookie` headers between semicolons
counts as one, unless it is empty or only spaces and tabs. counts as one, unless it is empty or only spaces and tabs.
- more than 100 header fields. A header sent twice counts twice. `Host` and - more than 100 header fields. A header sent twice counts twice. `Host` and
`Transfer-Encoding` do not count. `X-Request-ID` and `X-Forwarded-Proto` do. `Transfer-Encoding` do not count. `X-Request-ID` and `X-Forwarded-Proto` do.
Real clients never reach these limits. nginx refuses lines over 8 KiB by Real clients never reach these limits. The answer is `deny` with `431`, and the
default, and Apache's default limit is about the same. The answer is `deny`. The log line's action is `request_header_too_large`. The refusal makes no ban and is
log line's action is `request_line_too_long` for `414` and no offence.
`request_header_too_large` for `431`. The refusal makes no ban and is no
offence.
The header limits are the first check after `SWW_WAF_ALLOW_NETS`. A request over The header limits are the first check after `SWW_WAF_ALLOW_NETS`. A request over
them is refused before any later check runs. So it causes no lookup, no query to them is refused before any later check runs. So it causes no lookup, no query to
an outside source and no rule match, and the rate limits do not count it. A an outside source and no rule match, and the rate limits do not count it. A
client under a ban or in `SWW_WAF_DENY_NETS` also gets `414` or `431` for such a client under a ban or in `SWW_WAF_DENY_NETS` also gets `431` for such a request.
request.
The limits apply on every path and in every `SWW_WAF_CORE_RULE_SET_MODE`. A The limits apply on every path and in every `SWW_WAF_CORE_RULE_SET_MODE`. A
client in `SWW_WAF_ALLOW_NETS` skips them, as it skips the Core Rule Set. In client in `SWW_WAF_ALLOW_NETS` skips them, as it skips the Core Rule Set. In
@@ -2497,10 +2460,10 @@ client in `SWW_WAF_ALLOW_NETS` skips them, as it skips the Core Rule Set. In
other check runs on it. other check runs on it.
The Core Rule Set runs each of its rules on each cookie, so thousands of small The Core Rule Set runs each of its rules on each cookie, so thousands of small
cookies would keep it busy for over a second. One 30 KB cookie or query string cookies would keep it busy for over a second. Within the limits, one long cookie
would keep it busy for a third of a second or more. Within the limits, a request or query string can still keep it busy for a fraction of a second. So enough
takes less of its time. Enough such requests at once can still overload `waf` such requests at once can overload `waf` past
past `SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS`. `SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS`.
### The report ### The report
+2 -6
View File
@@ -102,8 +102,7 @@ Every one of these lines has:
ban this request made or made permanent; ban this request made or made permanent;
- `country_denied`, `client_is_rate_limited`, `blocked_by_block_rule` and - `country_denied`, `client_is_rate_limited`, `blocked_by_block_rule` and
`blocked_by_core_rule_set`: refused by that check; `blocked_by_core_rule_set`: refused by that check;
- `request_line_too_long`: a request line over the header limits; - `request_header_too_large`: over a header limit;
- `request_header_too_large`: headers over the header limits;
- `own_endpoint`: answered at one of `waf`'s own endpoints, or `404` for a - `own_endpoint`: answered at one of `waf`'s own endpoints, or `404` for a
path under `/_sww/`. path under `/_sww/`.
- `would_action`: in `observe` mode, what `enforce` mode would have done with a - `would_action`: in `observe` mode, what `enforce` mode would have done with a
@@ -127,10 +126,7 @@ Every one of these lines has:
- `rate_limits`: `window`, `count` and `limit`; - `rate_limits`: `window`, `count` and `limit`;
- `trap_paths`: `path`, the trap path asked for; - `trap_paths`: `path`, the trap path asked for;
- `rule_files`: `rule_id` and `rule_action`, of the rule that refused it; - `rule_files`: `rule_id` and `rule_action`, of the rule that refused it;
- `header_limits`: for a request line, `request_line_bytes` and - `header_limits`: `fields`, `fields_limit`, `cookies` and `cookies_limit`;
`request_line_bytes_limit`; for headers, `longest_header_line_bytes`,
`header_line_bytes_limit`, `fields`, `fields_limit`, `cookies` and
`cookies_limit`;
- `core_rule_set`: `score`, and `threshold`, - `core_rule_set`: `score`, and `threshold`,
`SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD`; `SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD`;
- `refusal_limit`: `window`, `count` and `limit`, of the refusal limit; - `refusal_limit`: `window`, `count` and `limit`, of the refusal limit;
+2 -3
View File
@@ -432,9 +432,8 @@ A few limits are fixed rather than settings:
- at most 20,000 anomaly counters are kept; - at most 20,000 anomaly counters are kept;
- each alert destination's queue holds at most 1,000 alerts; - each alert destination's queue holds at most 1,000 alerts;
- `waf` holds at most 10,000 answered requests awaiting their reports; - `waf` holds at most 10,000 answered requests awaiting their reports;
- `waf` refuses a request with a request line or header line over 8 KiB, more - `waf` refuses a request with more than 50 cookies or more than 100 header
than 50 cookies or more than 100 header fields (see "Answering in time" under fields (see "Answering in time" under "`waf`" in [`SPEC.md`](../SPEC.md));
"`waf`" in [`SPEC.md`](../SPEC.md));
- `rproxy` takes a request line and headers of at most 32 KiB, and closes a - `rproxy` takes a request line and headers of at most 32 KiB, and closes a
kept-open connection idle for 120 seconds; kept-open connection idle for 120 seconds;
- `rproxy` reads at most 1 MiB of the body of `waf`'s answer, and answers `502` - `rproxy` reads at most 1 MiB of the body of `waf`'s answer, and answers `502`
+3 -79
View File
@@ -172,13 +172,11 @@ SecRule REQUEST_FILENAME "@rx ^/(?:explore/repos|user/search_candidates|\
# A branch, tag or package name can hold a slash, as feat/docker-support and # A branch, tag or package name can hold a slash, as feat/docker-support and
# npm's @scope/name do. So q on a repository's branches, tags and packages # npm's @scope/name do. So q on a repository's branches, tags and packages
# pages, on an owner's packages page, on the admin page of packages, on the # pages, on an owner's packages page, on the admin page of packages and on the
# API's search of packages and on the Composer and Conan registries' searches # API's search of packages also skips this rule:
# also skips this rule:
# 932160: /bin/sh # 932160: /bin/sh
SecRule REQUEST_FILENAME "@rx ^/(?:[^/]+/[^/]+/(?:branches|tags|packages)|\ SecRule REQUEST_FILENAME "@rx ^/(?:[^/]+/[^/]+/(?:branches|tags|packages)|\
admin/packages|api/v1/packages/[^/]+|\ admin/packages|api/v1/packages/[^/]+)$" \
api/packages/[^/]+/(?:composer/search\.json|conan/v[12]/conans/search))$" \
"id:900605,phase:1,pass,nolog,ctl:ruleRemoveTargetById=932160;ARGS:q" "id:900605,phase:1,pass,nolog,ctl:ruleRemoveTargetById=932160;ARGS:q"
# gitea's other searches send a name or a few words in q too, which skips only # gitea's other searches send a name or a few words in q too, which skips only
@@ -193,8 +191,6 @@ SecRule REQUEST_FILENAME "@rx ^/(?:[^/]+/[^/]+/(?:branches|tags|packages)|\
# - the lists of users and organisations; # - the lists of users and organisations;
# - the API's searches of users, of topics, of an organisation's teams and of # - the API's searches of users, of topics, of an organisation's teams and of
# emails; # emails;
# - the searches of the Cargo, Chef, Composer and Conan registries, and the
# NuGet registry's search service;
# - the list of repositories a user watches; # - the list of repositories a user watches;
# - an organisation's milestones of one team, and its search for teams; # - an organisation's milestones of one team, and its search for teams;
# - the milestones of a repository or an organisation, and the projects of a # - the milestones of a repository or an organisation, and the projects of a
@@ -214,8 +210,6 @@ SecRule REQUEST_FILENAME "@rx ^/(?:repo/search|notifications/watching|\
explore/(?:users|organizations)|\ explore/(?:users|organizations)|\
api/v1/(?:users/search|topics/search|orgs/[^/]+/teams/search|\ api/v1/(?:users/search|topics/search|orgs/[^/]+/teams/search|\
admin/emails/search)|\ admin/emails/search)|\
api/packages/[^/]+/(?:cargo/api/v1/crates|chef/api/v1/(?:search|cookbooks)|\
composer/search\.json|conan/v[12]/conans/search|nuget/query)|\
org/[^/]+/(?:milestones/[^/]+|teams/-/search)|\ org/[^/]+/(?:milestones/[^/]+|teams/-/search)|\
[^/]+/-/packages/[^/]+/.+/versions|\ [^/]+/-/packages/[^/]+/.+/versions|\
[^/]+/[^/]+/(?:(?:issues|pulls)/posters|issues/suggestions|milestones|\ [^/]+/[^/]+/(?:(?:issues|pulls)/posters|issues/suggestions|milestones|\
@@ -252,76 +246,6 @@ SecRule REQUEST_FILENAME "@rx ^/api/v1/repos/[^/]+/[^/]+/milestones$" \
ctl:ruleRemoveTargetById=932250;ARGS:name,\ ctl:ruleRemoveTargetById=932250;ARGS:name,\
ctl:ruleRemoveTargetById=932260;ARGS:name" ctl:ruleRemoveTargetById=932260;ARGS:name"
# The npm registry's search sends its words in text, which skips these rules
# there:
# 930120: .htaccess
# 932120: powershell
# 932160: /bin/sh, as an npm name such as @alice/tools-cli holds a slash
# 932250: ls -la
# 932260: python3
SecRule REQUEST_FILENAME "@rx ^/api/packages/[^/]+/npm/-/v1/search$" \
"id:900609,phase:1,pass,nolog,\
ctl:ruleRemoveTargetById=930120;ARGS:text,\
ctl:ruleRemoveTargetById=932120;ARGS:text,\
ctl:ruleRemoveTargetById=932160;ARGS:text,\
ctl:ruleRemoveTargetById=932250;ARGS:text,\
ctl:ruleRemoveTargetById=932260;ARGS:text"
# A Conan search takes a pattern such as zlib/*, which the rule for SQL
# injection (942100) refused in q. q skips that rule only when the request has
# one q, in the query string or the body, with the shape of a Conan reference.
# A reference is a name, then optionally / and a version, then optionally @
# and a user, then optionally / and a channel. Each part is made of letters,
# digits, _, ., - and *. A part holds no +, since 942100 reads + as a space.
# q must also hold no */, which ends an SQL comment, and no --, which starts
# one. Last, 942100 must find no SQL injection in q once each comment that /*
# starts, an unclosed one included, is replaced by a space. So zlib/* passes,
# and SQL injection such as these stays refused:
# 1' OR '1'='1
# 1/**/or/**/1
# 1/*@*/or-1
# 1+or+1--
# 1-or-1--
# x/1.or.1
# 1.having.1
# This rule is in phase 2, because a phase 1 rule runs before the body is read
# and cannot see a q there. It checks tx.app_gitea, since the skip above does
# not reach phase 2.
SecRule REQUEST_FILENAME "@rx ^/api/packages/[^/]+/conan/v[12]/conans/search$" \
"id:900611,phase:2,pass,nolog,chain"
SecRule &TX:app_gitea "@eq 1" "chain"
SecRule &ARGS:q "@eq 1" "chain"
SecRule ARGS:q "!@contains */" "chain"
SecRule ARGS:q "!@contains --" "chain"
SecRule ARGS:q "!@detectSQLi" "t:none,t:replaceComments,chain"
SecRule ARGS:q "@rx ^[\w.*-]+(?:/[\w.*-]+)?(?:@[\w.*-]+(?:/[\w.*-]+)?)?$" \
"ctl:ruleRemoveTargetById=942100;ARGS:q"
# The NuGet v2 API's searches, Search() and Packages(), send their words in
# searchTerm, or in $filter as an expression such as
# tolower(Id) eq 'python3'. Search()/$count and Packages()/$count count a
# search's results and read the same fields. searchTerm skips these rules:
# 930120: .htaccess
# 932120: powershell
# 932250: ls -la
# 932260: python3
# $filter skips these:
# 930120: .htaccess
# 932120: powershell
# The names $filter, $orderby and $top skip the rule for MongoDB operators
# (942290), which refused every such search.
SecRule REQUEST_FILENAME "@rx ^/api/packages/[^/]+/nuget/(?:Search|Packages)\(\)(?:/\$count)?$" \
"id:900612,phase:1,pass,nolog,\
ctl:ruleRemoveTargetById=930120;ARGS:searchTerm,\
ctl:ruleRemoveTargetById=932120;ARGS:searchTerm,\
ctl:ruleRemoveTargetById=932250;ARGS:searchTerm,\
ctl:ruleRemoveTargetById=932260;ARGS:searchTerm,\
ctl:ruleRemoveTargetById=930120;ARGS:$filter,\
ctl:ruleRemoveTargetById=932120;ARGS:$filter,\
ctl:ruleRemoveTargetById=942290;ARGS_NAMES:$filter,\
ctl:ruleRemoveTargetById=942290;ARGS_NAMES:$orderby,\
ctl:ruleRemoveTargetById=942290;ARGS_NAMES:$top"
# The repositories on a user's or an organisation's page are searched with q # The repositories on a user's or an organisation's page are searched with q
# too. Their path has one segment, so q skips these rules on every path of one # too. Their path has one segment, so q skips these rules on every path of one
# segment, other apps' pages such as /index.php included: # segment, other apps' pages such as /index.php included:
+16 -167
View File
@@ -271,28 +271,15 @@ const giteaAPIPackageSearch = "/api/v1/packages/alice?q="
func TestGiteaAttacksAreRefusedInItsAPISearches(t *testing.T) { func TestGiteaAttacksAreRefusedInItsAPISearches(t *testing.T) {
t.Parallel() t.Parallel()
checkSearchesRefuseAttacks(
t,
giteaAPISearches,
[]string{giteaAPIPackageSearch},
)
}
// checkSearchesRefuseAttacks checks that SQL injection, a path traversal,
// script injection and a shell command stay refused in the field of each
// search, in the query string and in a form data body. Each search ends in
// its field. /bin/sh stays refused too in the field of each of
// refusingBinSh, but not of lettingBinSh.
func checkSearchesRefuseAttacks(
t *testing.T, refusingBinSh, lettingBinSh []string,
) {
t.Helper()
const threshold = 5 // SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default const threshold = 5 // SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default
defaults, bodies := atDefaults(t), readingBodies(t) defaults, bodies := atDefaults(t), readingBodies(t)
searches := slices.Concat(
giteaAPISearches,
[]string{giteaAPIPackageSearch},
)
for _, search := range slices.Concat(refusingBinSh, lettingBinSh) { for _, search := range searches {
path, field, _ := strings.Cut(search, "?") path, field, _ := strings.Cut(search, "?")
for _, attack := range []string{ for _, attack := range []string{
@@ -316,7 +303,8 @@ func checkSearchesRefuseAttacks(
} }
} }
for _, search := range refusingBinSh { // /bin/sh stays refused on the searches that are not of packages.
for _, search := range giteaAPISearches {
if got := inspect(t, defaults, get(search+binSh)); got.Score < threshold { if got := inspect(t, defaults, get(search+binSh)); got.Score < threshold {
t.Errorf("%s%s let through: %+v", search, binSh, got) t.Errorf("%s%s let through: %+v", search, binSh, got)
} }
@@ -326,9 +314,14 @@ func checkSearchesRefuseAttacks(
func TestGiteaAPISearchExclusionsAreNarrow(t *testing.T) { func TestGiteaAPISearchExclusionsAreNarrow(t *testing.T) {
t.Parallel() t.Parallel()
// Each search's field on paths near the searches, and another field on const threshold = 5 // SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default
// them.
checkValuesStayRefused(t, []string{ crs := atDefaults(t)
// /bin/sh, a shell command after a ";", a path traversal and the hard
// values stay refused in each search's field on paths near the
// searches, and in another field on them.
for _, target := range []string{
"/api/v1/topics/search/x?q=", "/api/v1/topics/search/x?q=",
"/api/v1/orgs/acme/teams?q=", "/api/v1/orgs/acme/teams?q=",
"/api/v1/orgs/acme/teams/search/x?q=", "/api/v1/orgs/acme/teams/search/x?q=",
@@ -344,19 +337,7 @@ func TestGiteaAPISearchExclusionsAreNarrow(t *testing.T) {
"/api/v1/admin/unadopted?q=", "/api/v1/admin/unadopted?q=",
"/api/v1/repos/alice/tools/milestones?q=", "/api/v1/repos/alice/tools/milestones?q=",
"/api/v1/packages/alice?other=", "/api/v1/packages/alice?other=",
}) } {
}
// checkValuesStayRefused checks that /bin/sh, a shell command after a ";", a
// path traversal and the hard values stay refused at the end of each target.
func checkValuesStayRefused(t *testing.T, targets []string) {
t.Helper()
const threshold = 5 // SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default
crs := atDefaults(t)
for _, target := range targets {
for _, value := range slices.Concat( for _, value := range slices.Concat(
[]string{binSh, commandInjection, pathTraversal}, []string{binSh, commandInjection, pathTraversal},
hardValues, hardValues,
@@ -367,135 +348,3 @@ func checkValuesStayRefused(t *testing.T, targets []string) {
} }
} }
} }
// giteaRegistrySearches are the package registries' searches whose field
// skips the lists of system files and command names. Each ends in the field
// it sends its words in.
//
//nolint:gochecknoglobals // a constant cannot be a list
var giteaRegistrySearches = []string{
"/api/packages/alice/cargo/api/v1/crates?q=",
"/api/packages/alice/chef/api/v1/search?q=",
"/api/packages/alice/chef/api/v1/cookbooks?q=",
"/api/packages/alice/nuget/query?q=",
"/api/packages/alice/nuget/Search()?searchTerm=",
"/api/packages/alice/nuget/Packages()?$filter=",
"/api/packages/alice/nuget/Search()/$count?searchTerm=",
"/api/packages/alice/nuget/Packages()/$count?$filter=",
}
// giteaRegistrySearchesLettingBinSh are the package registries' searches
// whose field also lets /bin/sh through, since a Composer, Conan or npm name
// can hold a slash.
//
//nolint:gochecknoglobals // a constant cannot be a list
var giteaRegistrySearchesLettingBinSh = []string{
"/api/packages/alice/composer/search.json?q=",
"/api/packages/alice/conan/v1/conans/search?q=",
"/api/packages/alice/conan/v2/conans/search?q=",
"/api/packages/alice/npm/-/v1/search?text=",
}
func TestGiteaAttacksAreRefusedInItsRegistrySearches(t *testing.T) {
t.Parallel()
checkSearchesRefuseAttacks(
t,
giteaRegistrySearches,
giteaRegistrySearchesLettingBinSh,
)
}
func TestGiteaRegistrySearchExclusionsAreNarrow(t *testing.T) {
t.Parallel()
// Each search's field on paths near the searches, and another field on
// them.
checkValuesStayRefused(t, []string{
"/api/packages/alice/cargo/api/v1/crates/tools-cli?q=",
"/api/packages/alice/chef/api/v1/cookbooks/tools?q=",
"/api/packages/alice/composer/packages.json?q=",
"/api/packages/alice/conan/v1/conans/tools/1.0/alice/stable/search?q=",
"/api/packages/alice/nuget/query/x?q=",
"/api/packages/alice/nuget/FindPackagesById()?id=",
"/api/packages/alice/nuget/FindPackagesById()/$count?id=",
"/api/packages/alice/nuget/Search()/$count/x?searchTerm=",
"/api/packages/alice/npm/tools-cli?text=",
"/api/packages/alice/cargo/api/v1/crates?other=",
"/api/packages/alice/chef/api/v1/search?other=",
"/api/packages/alice/composer/search.json?type=",
"/api/packages/alice/conan/v2/conans/search?other=",
"/api/packages/alice/nuget/query?other=",
"/api/packages/alice/nuget/Search()?targetFramework=",
"/api/packages/alice/nuget/Packages()?$orderby=",
"/api/packages/alice/nuget/Search()/$count?targetFramework=",
"/api/packages/alice/nuget/Packages()/$count?$orderby=",
"/api/packages/alice/npm/-/v1/search?q=",
})
const threshold = 5 // SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default
crs := atDefaults(t)
// The rule for MongoDB operators (942290) still refuses the name of
// another operator on NuGet's searches, and $filter on NuGet's other
// paths. The rule for SQL injection (942100) still refuses tools/* in a
// field other than a Conan search's, SQL injection in a second q after a
// Conan pattern, SQL comments or dots that stand in for spaces, and SQL
// injection holding + or --.
for _, target := range []string{
"/api/packages/alice/nuget/Search()?$where=1",
"/api/packages/alice/nuget/Packages()?$ne=1",
"/api/packages/alice/nuget/Search()/$count?$where=1",
"/api/packages/alice/nuget/FindPackagesById()?$filter=IsLatestVersion",
"/api/packages/alice/nuget/FindPackagesById()/$count?$filter=IsLatestVersion",
"/api/packages/alice/composer/search.json?q=tools%2F%2A",
"/api/packages/alice/conan/v2/conans/search?other=tools%2F%2A",
"/api/packages/alice/conan/v2/conans/search?q=tools%2F%2A&q=" +
sqlInjection,
"/api/packages/alice/conan/v2/conans/search?q=1%2F%2A%2A%2For%2F%2A%2A%2F1",
"/api/packages/alice/conan/v2/conans/search?q=1%2F%2A%40%2A%2For-1",
"/api/packages/alice/conan/v2/conans/search?q=1%2Bor%2B1--",
"/api/packages/alice/conan/v2/conans/search?q=1-or-1--",
"/api/packages/alice/conan/v2/conans/search?q=x%2F1.or.1",
"/api/packages/alice/conan/v2/conans/search?q=x%2F1.and.1",
"/api/packages/alice/conan/v2/conans/search?q=1.having.1",
} {
if got := inspect(t, crs, get(target)); got.Score < threshold {
t.Errorf("%s let through: %+v", target, got)
}
}
// SQL injection in a q in the body is refused, even after a Conan
// pattern in the query string.
search := request{
http.MethodPost,
"/api/packages/alice/conan/v2/conans/search?q=zlib",
[]string{formData},
}
body := "q=" + sqlInjection
if got, _ := inspectBody(t, readingBodies(t), search, body); got.Score < threshold {
t.Errorf("POST %s %s let through: %+v", search.target, body, got)
}
// So is SQL injection with dots for spaces, in a q sent only in the body.
search.target = "/api/packages/alice/conan/v2/conans/search"
body = "q=x%2F1.or.1"
if got, _ := inspectBody(t, readingBodies(t), search, body); got.Score < threshold {
t.Errorf("POST %s %s let through: %+v", search.target, body, got)
}
// The rule that lets a Conan pattern through is in phase 2, where it
// checks tx.app_gitea itself: a hostname that does not serve gitea
// refuses the pattern.
pattern := get(
"/api/packages/alice/conan/v2/conans/search?q=tools%2F%2A",
)
apps := servingApps(t, map[string][]string{
giteaHost: {gitea}, otherHost: {wordpress},
})
wantResult(t, apps, withHost(pattern, giteaHost), coreruleset.Result{})
wantResult(t, apps, withHost(pattern, otherHost), matched(942100))
}
-100
View File
@@ -1034,103 +1034,3 @@
{"method": "GET", "target": "/api/v1/repos/alice/tools/milestones?state=open&name=ls%20-la", "headers": ["Host: example.com", "User-Agent: curl/8.18.0", "Accept: */*", "Authorization: token 772deb2aefef4c9484e8c673197b82bc880fc7ca"], "body": ""} {"method": "GET", "target": "/api/v1/repos/alice/tools/milestones?state=open&name=ls%20-la", "headers": ["Host: example.com", "User-Agent: curl/8.18.0", "Accept: */*", "Authorization: token 772deb2aefef4c9484e8c673197b82bc880fc7ca"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/alice/tools/milestones?state=open&name=.htaccess", "headers": ["Host: example.com", "User-Agent: curl/8.18.0", "Accept: */*", "Authorization: token 772deb2aefef4c9484e8c673197b82bc880fc7ca"], "body": ""} {"method": "GET", "target": "/api/v1/repos/alice/tools/milestones?state=open&name=.htaccess", "headers": ["Host: example.com", "User-Agent: curl/8.18.0", "Accept: */*", "Authorization: token 772deb2aefef4c9484e8c673197b82bc880fc7ca"], "body": ""}
{"method": "GET", "target": "/api/v1/repos/alice/tools/milestones?state=open&name=v1.0", "headers": ["Host: example.com", "User-Agent: curl/8.18.0", "Accept: */*", "Authorization: token 772deb2aefef4c9484e8c673197b82bc880fc7ca"], "body": ""} {"method": "GET", "target": "/api/v1/repos/alice/tools/milestones?state=open&name=v1.0", "headers": ["Host: example.com", "User-Agent: curl/8.18.0", "Accept: */*", "Authorization: token 772deb2aefef4c9484e8c673197b82bc880fc7ca"], "body": ""}
# The package registries' searches, sent to Forgejo 17.0.1 by each registry's
# own client: npm, composer, conan 2 and 1, cargo, dotnet and knife. curl sent
# the requests no client sends: the Chef cookbook list with a search, and
# NuGet's Packages() with the search in $filter, as NuGet's client builds it for
# a feed without Search(). curl also sent each NuGet Search() and Packages()
# search to Search()/$count and Packages()/$count, which count its results. Each
# searches for words that name commands and files, such as python3 and
# .htaccess, and for plain words. A Composer, Conan or npm name can hold a
# slash, so those searches are also sent /bin/sh.
{"method": "GET", "target": "/api/packages/alice/npm/-/v1/search?text=python3&size=20&from=0&quality=0.65&popularity=0.98&maintenance=0.5", "headers": ["user-agent: npm/10.9.9 node/v22.23.5 linux x64 workspaces/false", "npm-auth-type: web", "npm-command: search", "Accept: */*", "Accept-Encoding: gzip,deflate", "Host: example.com", "connection: keep-alive"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/npm/-/v1/search?text=powershell&size=20&from=0&quality=0.65&popularity=0.98&maintenance=0.5", "headers": ["user-agent: npm/10.9.9 node/v22.23.5 linux x64 workspaces/false", "npm-auth-type: web", "npm-command: search", "Accept: */*", "Accept-Encoding: gzip,deflate", "Host: example.com", "connection: keep-alive"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/npm/-/v1/search?text=ls+-la&size=20&from=0&quality=0.65&popularity=0.98&maintenance=0.5", "headers": ["user-agent: npm/10.9.9 node/v22.23.5 linux x64 workspaces/false", "npm-auth-type: web", "npm-command: search", "Accept: */*", "Accept-Encoding: gzip,deflate", "Host: example.com", "connection: keep-alive"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/npm/-/v1/search?text=.htaccess&size=20&from=0&quality=0.65&popularity=0.98&maintenance=0.5", "headers": ["user-agent: npm/10.9.9 node/v22.23.5 linux x64 workspaces/false", "npm-auth-type: web", "npm-command: search", "Accept: */*", "Accept-Encoding: gzip,deflate", "Host: example.com", "connection: keep-alive"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/npm/-/v1/search?text=%2Fbin%2Fsh&size=20&from=0&quality=0.65&popularity=0.98&maintenance=0.5", "headers": ["user-agent: npm/10.9.9 node/v22.23.5 linux x64 workspaces/false", "npm-auth-type: web", "npm-command: search", "Accept: */*", "Accept-Encoding: gzip,deflate", "Host: example.com", "connection: keep-alive"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/npm/-/v1/search?text=%40alice%2Ftools-cli&size=20&from=0&quality=0.65&popularity=0.98&maintenance=0.5", "headers": ["user-agent: npm/10.9.9 node/v22.23.5 linux x64 workspaces/false", "npm-auth-type: web", "npm-command: search", "Accept: */*", "Accept-Encoding: gzip,deflate", "Host: example.com", "connection: keep-alive"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/npm/-/v1/search?text=docker-compose.yml&size=20&from=0&quality=0.65&popularity=0.98&maintenance=0.5", "headers": ["user-agent: npm/10.9.9 node/v22.23.5 linux x64 workspaces/false", "npm-auth-type: web", "npm-command: search", "Accept: */*", "Accept-Encoding: gzip,deflate", "Host: example.com", "connection: keep-alive"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/npm/-/v1/search?text=tools&size=20&from=0&quality=0.65&popularity=0.98&maintenance=0.5", "headers": ["user-agent: npm/10.9.9 node/v22.23.5 linux x64 workspaces/false", "npm-auth-type: web", "npm-command: search", "Accept: */*", "Accept-Encoding: gzip,deflate", "Host: example.com", "connection: keep-alive"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/composer/search.json?q=python3&type=", "headers": ["Host: example.com", "Accept: */*", "Accept-Encoding: deflate, gzip, br, zstd", "User-Agent: Composer/2.11.2 (Linux; 6.8.0-111-generic; PHP 8.5.12; cURL 8.23.0; cmd:search)", "Connection: keep-alive"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/composer/search.json?q=powershell&type=", "headers": ["Host: example.com", "Accept: */*", "Accept-Encoding: deflate, gzip, br, zstd", "User-Agent: Composer/2.11.2 (Linux; 6.8.0-111-generic; PHP 8.5.12; cURL 8.23.0; cmd:search)", "Connection: keep-alive"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/composer/search.json?q=ls+-la&type=", "headers": ["Host: example.com", "Accept: */*", "Accept-Encoding: deflate, gzip, br, zstd", "User-Agent: Composer/2.11.2 (Linux; 6.8.0-111-generic; PHP 8.5.12; cURL 8.23.0; cmd:search)", "Connection: keep-alive"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/composer/search.json?q=.htaccess&type=", "headers": ["Host: example.com", "Accept: */*", "Accept-Encoding: deflate, gzip, br, zstd", "User-Agent: Composer/2.11.2 (Linux; 6.8.0-111-generic; PHP 8.5.12; cURL 8.23.0; cmd:search)", "Connection: keep-alive"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/composer/search.json?q=%2Fbin%2Fsh&type=", "headers": ["Host: example.com", "Accept: */*", "Accept-Encoding: deflate, gzip, br, zstd", "User-Agent: Composer/2.11.2 (Linux; 6.8.0-111-generic; PHP 8.5.12; cURL 8.23.0; cmd:search)", "Connection: keep-alive"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/composer/search.json?q=alice%2Ftools-cli&type=", "headers": ["Host: example.com", "Accept: */*", "Accept-Encoding: deflate, gzip, br, zstd", "User-Agent: Composer/2.11.2 (Linux; 6.8.0-111-generic; PHP 8.5.12; cURL 8.23.0; cmd:search)", "Connection: keep-alive"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/composer/search.json?q=docker-compose.yml&type=", "headers": ["Host: example.com", "Accept: */*", "Accept-Encoding: deflate, gzip, br, zstd", "User-Agent: Composer/2.11.2 (Linux; 6.8.0-111-generic; PHP 8.5.12; cURL 8.23.0; cmd:search)", "Connection: keep-alive"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/composer/search.json?q=tools&type=", "headers": ["Host: example.com", "Accept: */*", "Accept-Encoding: deflate, gzip, br, zstd", "User-Agent: Composer/2.11.2 (Linux; 6.8.0-111-generic; PHP 8.5.12; cURL 8.23.0; cmd:search)", "Connection: keep-alive"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/conan/v2/conans/search?q=python3", "headers": ["Host: example.com", "User-Agent: Conan/2.34.0 (Linux 6.8.0-111-generic; Python 3.13.18; x86_64)", "Accept-Encoding: gzip, deflate", "Accept: */*", "Connection: keep-alive", "X-Client-Anonymous-Id: afce410baeaee94a86997417d92f7788a91b00c5"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/conan/v2/conans/search?q=powershell", "headers": ["Host: example.com", "User-Agent: Conan/2.34.0 (Linux 6.8.0-111-generic; Python 3.13.18; x86_64)", "Accept-Encoding: gzip, deflate", "Accept: */*", "Connection: keep-alive", "X-Client-Anonymous-Id: afce410baeaee94a86997417d92f7788a91b00c5"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/conan/v2/conans/search?q=ls+-la", "headers": ["Host: example.com", "User-Agent: Conan/2.34.0 (Linux 6.8.0-111-generic; Python 3.13.18; x86_64)", "Accept-Encoding: gzip, deflate", "Accept: */*", "Connection: keep-alive", "X-Client-Anonymous-Id: afce410baeaee94a86997417d92f7788a91b00c5"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/conan/v2/conans/search?q=.htaccess", "headers": ["Host: example.com", "User-Agent: Conan/2.34.0 (Linux 6.8.0-111-generic; Python 3.13.18; x86_64)", "Accept-Encoding: gzip, deflate", "Accept: */*", "Connection: keep-alive", "X-Client-Anonymous-Id: afce410baeaee94a86997417d92f7788a91b00c5"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/conan/v2/conans/search?q=%2Fbin%2Fsh", "headers": ["Host: example.com", "User-Agent: Conan/2.34.0 (Linux 6.8.0-111-generic; Python 3.13.18; x86_64)", "Accept-Encoding: gzip, deflate", "Accept: */*", "Connection: keep-alive", "X-Client-Anonymous-Id: afce410baeaee94a86997417d92f7788a91b00c5"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/conan/v2/conans/search?q=tools%2F%2A", "headers": ["Host: example.com", "User-Agent: Conan/2.34.0 (Linux 6.8.0-111-generic; Python 3.13.18; x86_64)", "Accept-Encoding: gzip, deflate", "Accept: */*", "Connection: keep-alive", "X-Client-Anonymous-Id: afce410baeaee94a86997417d92f7788a91b00c5"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/conan/v2/conans/search?q=tools%2F1.0%40alice%2Fstable", "headers": ["Host: example.com", "User-Agent: Conan/2.34.0 (Linux 6.8.0-111-generic; Python 3.13.18; x86_64)", "Accept-Encoding: gzip, deflate", "Accept: */*", "Connection: keep-alive", "X-Client-Anonymous-Id: afce410baeaee94a86997417d92f7788a91b00c5"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/conan/v2/conans/search?q=docker-compose.yml", "headers": ["Host: example.com", "User-Agent: Conan/2.34.0 (Linux 6.8.0-111-generic; Python 3.13.18; x86_64)", "Accept-Encoding: gzip, deflate", "Accept: */*", "Connection: keep-alive", "X-Client-Anonymous-Id: afce410baeaee94a86997417d92f7788a91b00c5"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/conan/v2/conans/search?q=tools", "headers": ["Host: example.com", "User-Agent: Conan/2.34.0 (Linux 6.8.0-111-generic; Python 3.13.18; x86_64)", "Accept-Encoding: gzip, deflate", "Accept: */*", "Connection: keep-alive", "X-Client-Anonymous-Id: afce410baeaee94a86997417d92f7788a91b00c5"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/conan/v1/conans/search?q=python3", "headers": ["Host: example.com", "User-Agent: Conan/1.66.0 (Linux 6.8.0-111-generic; Python 3.13.18; x86_64)", "Accept-Encoding: gzip, deflate", "Accept: */*", "Connection: keep-alive", "X-Client-Anonymous-Id: afce410baeaee94a86997417d92f7788a91b00c5", "X-Client-Id: "], "body": ""}
{"method": "GET", "target": "/api/packages/alice/conan/v1/conans/search?q=powershell", "headers": ["Host: example.com", "User-Agent: Conan/1.66.0 (Linux 6.8.0-111-generic; Python 3.13.18; x86_64)", "Accept-Encoding: gzip, deflate", "Accept: */*", "Connection: keep-alive", "X-Client-Anonymous-Id: afce410baeaee94a86997417d92f7788a91b00c5", "X-Client-Id: "], "body": ""}
{"method": "GET", "target": "/api/packages/alice/conan/v1/conans/search?q=ls+-la", "headers": ["Host: example.com", "User-Agent: Conan/1.66.0 (Linux 6.8.0-111-generic; Python 3.13.18; x86_64)", "Accept-Encoding: gzip, deflate", "Accept: */*", "Connection: keep-alive", "X-Client-Anonymous-Id: afce410baeaee94a86997417d92f7788a91b00c5", "X-Client-Id: "], "body": ""}
{"method": "GET", "target": "/api/packages/alice/conan/v1/conans/search?q=.htaccess", "headers": ["Host: example.com", "User-Agent: Conan/1.66.0 (Linux 6.8.0-111-generic; Python 3.13.18; x86_64)", "Accept-Encoding: gzip, deflate", "Accept: */*", "Connection: keep-alive", "X-Client-Anonymous-Id: afce410baeaee94a86997417d92f7788a91b00c5", "X-Client-Id: "], "body": ""}
{"method": "GET", "target": "/api/packages/alice/conan/v1/conans/search?q=%2Fbin%2Fsh", "headers": ["Host: example.com", "User-Agent: Conan/1.66.0 (Linux 6.8.0-111-generic; Python 3.13.18; x86_64)", "Accept-Encoding: gzip, deflate", "Accept: */*", "Connection: keep-alive", "X-Client-Anonymous-Id: afce410baeaee94a86997417d92f7788a91b00c5", "X-Client-Id: "], "body": ""}
{"method": "GET", "target": "/api/packages/alice/conan/v1/conans/search?q=tools%2F%2A", "headers": ["Host: example.com", "User-Agent: Conan/1.66.0 (Linux 6.8.0-111-generic; Python 3.13.18; x86_64)", "Accept-Encoding: gzip, deflate", "Accept: */*", "Connection: keep-alive", "X-Client-Anonymous-Id: afce410baeaee94a86997417d92f7788a91b00c5", "X-Client-Id: "], "body": ""}
{"method": "GET", "target": "/api/packages/alice/conan/v1/conans/search?q=docker-compose.yml", "headers": ["Host: example.com", "User-Agent: Conan/1.66.0 (Linux 6.8.0-111-generic; Python 3.13.18; x86_64)", "Accept-Encoding: gzip, deflate", "Accept: */*", "Connection: keep-alive", "X-Client-Anonymous-Id: afce410baeaee94a86997417d92f7788a91b00c5", "X-Client-Id: "], "body": ""}
{"method": "GET", "target": "/api/packages/alice/conan/v1/conans/search?q=tools", "headers": ["Host: example.com", "User-Agent: Conan/1.66.0 (Linux 6.8.0-111-generic; Python 3.13.18; x86_64)", "Accept-Encoding: gzip, deflate", "Accept: */*", "Connection: keep-alive", "X-Client-Anonymous-Id: afce410baeaee94a86997417d92f7788a91b00c5", "X-Client-Id: "], "body": ""}
{"method": "GET", "target": "/api/packages/alice/cargo/api/v1/crates?q=python3&per_page=10", "headers": ["Host: example.com", "User-Agent: cargo/1.99.1 (5f94df478 2026-08-27)", "Accept-Encoding: deflate, gzip", "Upgrade: h2c", "HTTP2-Settings: AAMAAABkAAQAAQAAAAIAAAAA", "accept: application/json", "Connection: Upgrade, HTTP2-Settings"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/cargo/api/v1/crates?q=powershell&per_page=10", "headers": ["Host: example.com", "User-Agent: cargo/1.99.1 (5f94df478 2026-08-27)", "Accept-Encoding: deflate, gzip", "Upgrade: h2c", "HTTP2-Settings: AAMAAABkAAQAAQAAAAIAAAAA", "accept: application/json", "Connection: Upgrade, HTTP2-Settings"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/cargo/api/v1/crates?q=ls%20%2Dla&per_page=10", "headers": ["Host: example.com", "User-Agent: cargo/1.99.1 (5f94df478 2026-08-27)", "Accept-Encoding: deflate, gzip", "Upgrade: h2c", "HTTP2-Settings: AAMAAABkAAQAAQAAAAIAAAAA", "accept: application/json", "Connection: Upgrade, HTTP2-Settings"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/cargo/api/v1/crates?q=%2Ehtaccess&per_page=10", "headers": ["Host: example.com", "User-Agent: cargo/1.99.1 (5f94df478 2026-08-27)", "Accept-Encoding: deflate, gzip", "Upgrade: h2c", "HTTP2-Settings: AAMAAABkAAQAAQAAAAIAAAAA", "accept: application/json", "Connection: Upgrade, HTTP2-Settings"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/cargo/api/v1/crates?q=docker%2Dcompose%2Eyml&per_page=10", "headers": ["Host: example.com", "User-Agent: cargo/1.99.1 (5f94df478 2026-08-27)", "Accept-Encoding: deflate, gzip", "Upgrade: h2c", "HTTP2-Settings: AAMAAABkAAQAAQAAAAIAAAAA", "accept: application/json", "Connection: Upgrade, HTTP2-Settings"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/cargo/api/v1/crates?q=tools&per_page=10", "headers": ["Host: example.com", "User-Agent: cargo/1.99.1 (5f94df478 2026-08-27)", "Accept-Encoding: deflate, gzip", "Upgrade: h2c", "HTTP2-Settings: AAMAAABkAAQAAQAAAAIAAAAA", "accept: application/json", "Connection: Upgrade, HTTP2-Settings"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/query?q=python3&skip=0&take=20&prerelease=false&semVerLevel=2.0.0", "headers": ["Host: example.com", "X-NuGet-Session-Id: bc433449-14e0-4cb0-907b-fcca1ead302b", "User-Agent: NuGet Client V3/6.14.3 (LINUX)", "X-NuGet-Client-Version: 6.14.3", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/query?q=powershell&skip=0&take=20&prerelease=false&semVerLevel=2.0.0", "headers": ["Host: example.com", "X-NuGet-Session-Id: e691cf8b-1859-45b1-aa67-e0987eb7474a", "User-Agent: NuGet Client V3/6.14.3 (LINUX)", "X-NuGet-Client-Version: 6.14.3", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/query?q=ls%20-la&skip=0&take=20&prerelease=false&semVerLevel=2.0.0", "headers": ["Host: example.com", "X-NuGet-Session-Id: 725a8ab1-a2c3-4924-84dd-a741bd6b8c07", "User-Agent: NuGet Client V3/6.14.3 (LINUX)", "X-NuGet-Client-Version: 6.14.3", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/query?q=.htaccess&skip=0&take=20&prerelease=false&semVerLevel=2.0.0", "headers": ["Host: example.com", "X-NuGet-Session-Id: 5b1d333f-6583-4f78-9c23-d776ebbde5cf", "User-Agent: NuGet Client V3/6.14.3 (LINUX)", "X-NuGet-Client-Version: 6.14.3", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/query?q=docker-compose.yml&skip=0&take=20&prerelease=false&semVerLevel=2.0.0", "headers": ["Host: example.com", "X-NuGet-Session-Id: 23a81a25-0828-4311-8d2d-66f0d5161b6b", "User-Agent: NuGet Client V3/6.14.3 (LINUX)", "X-NuGet-Client-Version: 6.14.3", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/query?q=tools&skip=0&take=20&prerelease=false&semVerLevel=2.0.0", "headers": ["Host: example.com", "X-NuGet-Session-Id: ebf39a79-969d-4ac5-b5a0-5119c2d43ccc", "User-Agent: NuGet Client V3/6.14.3 (LINUX)", "X-NuGet-Client-Version: 6.14.3", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Search()?$filter=IsLatestVersion&searchTerm='python3'&targetFramework=''&includePrerelease=false&$skip=0&$top=20&semVerLevel=2.0.0", "headers": ["Host: example.com", "Accept: application/atom+xml, application/xml", "X-NuGet-Session-Id: 825cbe04-63e7-414a-8394-2ecf74c68211", "User-Agent: NuGet Client V3/6.14.3 (LINUX)", "X-NuGet-Client-Version: 6.14.3", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Search()?$filter=IsLatestVersion&searchTerm='powershell'&targetFramework=''&includePrerelease=false&$skip=0&$top=20&semVerLevel=2.0.0", "headers": ["Host: example.com", "Accept: application/atom+xml, application/xml", "X-NuGet-Session-Id: f61bfff6-1c26-4024-bc17-6b78dcef2edc", "User-Agent: NuGet Client V3/6.14.3 (LINUX)", "X-NuGet-Client-Version: 6.14.3", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Search()?$filter=IsLatestVersion&searchTerm='ls%20-la'&targetFramework=''&includePrerelease=false&$skip=0&$top=20&semVerLevel=2.0.0", "headers": ["Host: example.com", "Accept: application/atom+xml, application/xml", "X-NuGet-Session-Id: e894f984-8abf-4a23-960a-d1a09c94eaa3", "User-Agent: NuGet Client V3/6.14.3 (LINUX)", "X-NuGet-Client-Version: 6.14.3", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Search()?$filter=IsLatestVersion&searchTerm='.htaccess'&targetFramework=''&includePrerelease=false&$skip=0&$top=20&semVerLevel=2.0.0", "headers": ["Host: example.com", "Accept: application/atom+xml, application/xml", "X-NuGet-Session-Id: 33e3aa1b-14cb-4854-821a-c3eb9e5be7ee", "User-Agent: NuGet Client V3/6.14.3 (LINUX)", "X-NuGet-Client-Version: 6.14.3", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Search()?$filter=IsLatestVersion&searchTerm='docker-compose.yml'&targetFramework=''&includePrerelease=false&$skip=0&$top=20&semVerLevel=2.0.0", "headers": ["Host: example.com", "Accept: application/atom+xml, application/xml", "X-NuGet-Session-Id: c478a564-7cf9-4282-9f00-bf5b7966607d", "User-Agent: NuGet Client V3/6.14.3 (LINUX)", "X-NuGet-Client-Version: 6.14.3", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Search()?$filter=IsLatestVersion&searchTerm='tools'&targetFramework=''&includePrerelease=false&$skip=0&$top=20&semVerLevel=2.0.0", "headers": ["Host: example.com", "Accept: application/atom+xml, application/xml", "X-NuGet-Session-Id: 8dfa5583-4005-4134-9343-ddb4c68c125b", "User-Agent: NuGet Client V3/6.14.3 (LINUX)", "X-NuGet-Client-Version: 6.14.3", "Accept-Encoding: gzip, deflate"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/chef/api/v1/search?q=python3&items=9999999&start=0", "headers": ["Accept: application/json", "Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3", "X-Remote-Request-Id: e7210054-9f40-4a58-8228-beeab1e3bb05", "X-Chef-Version: 19.4.46", "Host: example.com", "User-Agent: Cinc Client Knife/19.4.46 (ruby-3.4.12-p148; ohai-19.1.40; x86_64-linux; +https://cinc.sh)", "Connection: close"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/chef/api/v1/search?q=powershell&items=9999999&start=0", "headers": ["Accept: application/json", "Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3", "X-Remote-Request-Id: 0f3b26a8-7ca2-4b52-a9bb-c23a25c4172c", "X-Chef-Version: 19.4.46", "Host: example.com", "User-Agent: Cinc Client Knife/19.4.46 (ruby-3.4.12-p148; ohai-19.1.40; x86_64-linux; +https://cinc.sh)", "Connection: close"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/chef/api/v1/search?q=ls%20-la&items=9999999&start=0", "headers": ["Accept: application/json", "Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3", "X-Remote-Request-Id: d0fa60d5-6948-4149-845f-e38d340cc09a", "X-Chef-Version: 19.4.46", "Host: example.com", "User-Agent: Cinc Client Knife/19.4.46 (ruby-3.4.12-p148; ohai-19.1.40; x86_64-linux; +https://cinc.sh)", "Connection: close"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/chef/api/v1/search?q=.htaccess&items=9999999&start=0", "headers": ["Accept: application/json", "Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3", "X-Remote-Request-Id: 8c22d5d2-1691-4488-89ca-a405dd7a15cc", "X-Chef-Version: 19.4.46", "Host: example.com", "User-Agent: Cinc Client Knife/19.4.46 (ruby-3.4.12-p148; ohai-19.1.40; x86_64-linux; +https://cinc.sh)", "Connection: close"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/chef/api/v1/search?q=docker-compose.yml&items=9999999&start=0", "headers": ["Accept: application/json", "Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3", "X-Remote-Request-Id: 0a1bd4b1-0009-4d1d-931d-d1c19b26dd8b", "X-Chef-Version: 19.4.46", "Host: example.com", "User-Agent: Cinc Client Knife/19.4.46 (ruby-3.4.12-p148; ohai-19.1.40; x86_64-linux; +https://cinc.sh)", "Connection: close"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/chef/api/v1/search?q=tools&items=9999999&start=0", "headers": ["Accept: application/json", "Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3", "X-Remote-Request-Id: f858e4b5-10bc-4633-a517-f165a65ac4cb", "X-Chef-Version: 19.4.46", "Host: example.com", "User-Agent: Cinc Client Knife/19.4.46 (ruby-3.4.12-p148; ohai-19.1.40; x86_64-linux; +https://cinc.sh)", "Connection: close"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/chef/api/v1/cookbooks?q=python3&items=9999999&start=0", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/chef/api/v1/cookbooks?q=powershell&items=9999999&start=0", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/chef/api/v1/cookbooks?q=ls%20-la&items=9999999&start=0", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/chef/api/v1/cookbooks?q=.htaccess&items=9999999&start=0", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/chef/api/v1/cookbooks?q=docker-compose.yml&items=9999999&start=0", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/chef/api/v1/cookbooks?q=tools&items=9999999&start=0", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: application/json"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Packages()?$filter=((((Id%20ne%20null)%20and%20substringof('python3',tolower(Id)))%20or%20((Description%20ne%20null)%20and%20substringof('python3',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20python3%20',tolower(Tags))))%20and%20IsLatestVersion&$orderby=Id&$skip=0&$top=30&semVerLevel=2.0.0", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: application/atom+xml, application/xml"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Packages()?$filter=((((Id%20ne%20null)%20and%20substringof('powershell',tolower(Id)))%20or%20((Description%20ne%20null)%20and%20substringof('powershell',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20powershell%20',tolower(Tags))))%20and%20IsLatestVersion&$orderby=Id&$skip=0&$top=30&semVerLevel=2.0.0", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: application/atom+xml, application/xml"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Packages()?$filter=((((Id%20ne%20null)%20and%20substringof('.htaccess',tolower(Id)))%20or%20((Description%20ne%20null)%20and%20substringof('.htaccess',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20.htaccess%20',tolower(Tags))))%20and%20IsLatestVersion&$orderby=Id&$skip=0&$top=30&semVerLevel=2.0.0", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: application/atom+xml, application/xml"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Packages()?$filter=((((Id%20ne%20null)%20and%20substringof('docker-compose.yml',tolower(Id)))%20or%20((Description%20ne%20null)%20and%20substringof('docker-compose.yml',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20docker-compose.yml%20',tolower(Tags))))%20and%20IsLatestVersion&$orderby=Id&$skip=0&$top=30&semVerLevel=2.0.0", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: application/atom+xml, application/xml"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Packages()?$filter=((((Id%20ne%20null)%20and%20substringof('tools',tolower(Id)))%20or%20((Description%20ne%20null)%20and%20substringof('tools',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20tools%20',tolower(Tags))))%20and%20IsLatestVersion&$orderby=Id&$skip=0&$top=30&semVerLevel=2.0.0", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: application/atom+xml, application/xml"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Packages()?$filter=(((((((Id%20ne%20null)%20and%20substringof('ls',tolower(Id)))%20or%20((Description%20ne%20null)%20and%20substringof('ls',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20ls%20',tolower(Tags))))%20or%20((Id%20ne%20null)%20and%20substringof('-la',tolower(Id))))%20or%20((Description%20ne%20null)%20and%20substringof('-la',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20-la%20',tolower(Tags))))%20and%20IsLatestVersion&$orderby=Id&$skip=0&$top=30&semVerLevel=2.0.0", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: application/atom+xml, application/xml"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Packages()?$filter=tolower(Id)%20eq%20'python3'&$top=30", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: application/atom+xml, application/xml"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Packages()?$filter=tolower(Id)%20eq%20'powershell'&$top=30", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: application/atom+xml, application/xml"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Packages()?$filter=tolower(Id)%20eq%20'.htaccess'&$top=30", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: application/atom+xml, application/xml"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Packages()?$filter=tolower(Id)%20eq%20'docker-compose.yml'&$top=30", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: application/atom+xml, application/xml"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Packages()?$filter=tolower(Id)%20eq%20'tools'&$top=30", "headers": ["Host: example.com", "User-Agent: curl/8.17.0", "Accept: application/atom+xml, application/xml"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Search()/$count?$filter=IsLatestVersion&searchTerm='python3'&targetFramework=''&includePrerelease=false&$skip=0&$top=20&semVerLevel=2.0.0", "headers": ["Host: example.com", "User-Agent: curl/8.18.0", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Search()/$count?$filter=IsLatestVersion&searchTerm='powershell'&targetFramework=''&includePrerelease=false&$skip=0&$top=20&semVerLevel=2.0.0", "headers": ["Host: example.com", "User-Agent: curl/8.18.0", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Search()/$count?$filter=IsLatestVersion&searchTerm='ls%20-la'&targetFramework=''&includePrerelease=false&$skip=0&$top=20&semVerLevel=2.0.0", "headers": ["Host: example.com", "User-Agent: curl/8.18.0", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Search()/$count?$filter=IsLatestVersion&searchTerm='.htaccess'&targetFramework=''&includePrerelease=false&$skip=0&$top=20&semVerLevel=2.0.0", "headers": ["Host: example.com", "User-Agent: curl/8.18.0", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Search()/$count?$filter=IsLatestVersion&searchTerm='docker-compose.yml'&targetFramework=''&includePrerelease=false&$skip=0&$top=20&semVerLevel=2.0.0", "headers": ["Host: example.com", "User-Agent: curl/8.18.0", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Search()/$count?$filter=IsLatestVersion&searchTerm='tools'&targetFramework=''&includePrerelease=false&$skip=0&$top=20&semVerLevel=2.0.0", "headers": ["Host: example.com", "User-Agent: curl/8.18.0", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Packages()/$count?$filter=((((Id%20ne%20null)%20and%20substringof('python3',tolower(Id)))%20or%20((Description%20ne%20null)%20and%20substringof('python3',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20python3%20',tolower(Tags))))%20and%20IsLatestVersion&$orderby=Id&$skip=0&$top=30&semVerLevel=2.0.0", "headers": ["Host: example.com", "User-Agent: curl/8.18.0", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Packages()/$count?$filter=((((Id%20ne%20null)%20and%20substringof('powershell',tolower(Id)))%20or%20((Description%20ne%20null)%20and%20substringof('powershell',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20powershell%20',tolower(Tags))))%20and%20IsLatestVersion&$orderby=Id&$skip=0&$top=30&semVerLevel=2.0.0", "headers": ["Host: example.com", "User-Agent: curl/8.18.0", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Packages()/$count?$filter=(((((((Id%20ne%20null)%20and%20substringof('ls',tolower(Id)))%20or%20((Description%20ne%20null)%20and%20substringof('ls',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20ls%20',tolower(Tags))))%20or%20((Id%20ne%20null)%20and%20substringof('-la',tolower(Id))))%20or%20((Description%20ne%20null)%20and%20substringof('-la',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20-la%20',tolower(Tags))))%20and%20IsLatestVersion&$orderby=Id&$skip=0&$top=30&semVerLevel=2.0.0", "headers": ["Host: example.com", "User-Agent: curl/8.18.0", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Packages()/$count?$filter=((((Id%20ne%20null)%20and%20substringof('.htaccess',tolower(Id)))%20or%20((Description%20ne%20null)%20and%20substringof('.htaccess',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20.htaccess%20',tolower(Tags))))%20and%20IsLatestVersion&$orderby=Id&$skip=0&$top=30&semVerLevel=2.0.0", "headers": ["Host: example.com", "User-Agent: curl/8.18.0", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Packages()/$count?$filter=((((Id%20ne%20null)%20and%20substringof('docker-compose.yml',tolower(Id)))%20or%20((Description%20ne%20null)%20and%20substringof('docker-compose.yml',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20docker-compose.yml%20',tolower(Tags))))%20and%20IsLatestVersion&$orderby=Id&$skip=0&$top=30&semVerLevel=2.0.0", "headers": ["Host: example.com", "User-Agent: curl/8.18.0", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Packages()/$count?$filter=((((Id%20ne%20null)%20and%20substringof('tools',tolower(Id)))%20or%20((Description%20ne%20null)%20and%20substringof('tools',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20tools%20',tolower(Tags))))%20and%20IsLatestVersion&$orderby=Id&$skip=0&$top=30&semVerLevel=2.0.0", "headers": ["Host: example.com", "User-Agent: curl/8.18.0", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Packages()/$count?$filter=tolower(Id)%20eq%20'python3'&$top=30", "headers": ["Host: example.com", "User-Agent: curl/8.18.0", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Packages()/$count?$filter=tolower(Id)%20eq%20'powershell'&$top=30", "headers": ["Host: example.com", "User-Agent: curl/8.18.0", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Packages()/$count?$filter=tolower(Id)%20eq%20'.htaccess'&$top=30", "headers": ["Host: example.com", "User-Agent: curl/8.18.0", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Packages()/$count?$filter=tolower(Id)%20eq%20'docker-compose.yml'&$top=30", "headers": ["Host: example.com", "User-Agent: curl/8.18.0", "Accept: */*"], "body": ""}
{"method": "GET", "target": "/api/packages/alice/nuget/Packages()/$count?$filter=tolower(Id)%20eq%20'tools'&$top=30", "headers": ["Host: example.com", "User-Agent: curl/8.18.0", "Accept: */*"], "body": ""}
-100
View File
@@ -442,106 +442,6 @@
/api/v1/repos/alice/tools/milestones?state=open&name=.htaccess /api/v1/repos/alice/tools/milestones?state=open&name=.htaccess
/api/v1/repos/alice/tools/milestones?state=open&name=v1.0 /api/v1/repos/alice/tools/milestones?state=open&name=v1.0
# The package registries' searches, sent to gitea 1.27.3 by each registry's own
# client: cargo, knife, composer, conan 1 and 2, dotnet and npm. curl sent the
# requests no client sends: the Chef cookbook list with a search, and NuGet's
# Packages() with the search in $filter, as NuGet's client builds it for a feed
# without Search(). curl also sent each NuGet Search() and Packages() search to
# Search()/$count and Packages()/$count, which count its results. Each searches
# for words that name commands and files, such as python3 and .htaccess, and
# for plain words. A Composer, Conan or npm name can hold a slash, so those
# searches are also sent /bin/sh.
/api/packages/alice/cargo/api/v1/crates?q=python3&per_page=10
/api/packages/alice/cargo/api/v1/crates?q=powershell&per_page=10
/api/packages/alice/cargo/api/v1/crates?q=ls%20%2Dla&per_page=10
/api/packages/alice/cargo/api/v1/crates?q=%2Ehtaccess&per_page=10
/api/packages/alice/cargo/api/v1/crates?q=docker%2Dcompose%2Eyml&per_page=10
/api/packages/alice/cargo/api/v1/crates?q=tools&per_page=10
/api/packages/alice/chef/api/v1/search?q=python3&items=9999999&start=0
/api/packages/alice/chef/api/v1/search?q=powershell&items=9999999&start=0
/api/packages/alice/chef/api/v1/search?q=ls%20-la&items=9999999&start=0
/api/packages/alice/chef/api/v1/search?q=.htaccess&items=9999999&start=0
/api/packages/alice/chef/api/v1/search?q=docker-compose.yml&items=9999999&start=0
/api/packages/alice/chef/api/v1/search?q=tools&items=9999999&start=0
/api/packages/alice/chef/api/v1/cookbooks?q=python3&items=9999999&start=0
/api/packages/alice/chef/api/v1/cookbooks?q=powershell&items=9999999&start=0
/api/packages/alice/chef/api/v1/cookbooks?q=ls%20-la&items=9999999&start=0
/api/packages/alice/chef/api/v1/cookbooks?q=.htaccess&items=9999999&start=0
/api/packages/alice/chef/api/v1/cookbooks?q=docker-compose.yml&items=9999999&start=0
/api/packages/alice/chef/api/v1/cookbooks?q=tools&items=9999999&start=0
/api/packages/alice/composer/search.json?q=python3&type=
/api/packages/alice/composer/search.json?q=powershell&type=
/api/packages/alice/composer/search.json?q=ls+-la&type=
/api/packages/alice/composer/search.json?q=.htaccess&type=
/api/packages/alice/composer/search.json?q=%2Fbin%2Fsh&type=
/api/packages/alice/composer/search.json?q=alice%2Ftools-cli&type=
/api/packages/alice/composer/search.json?q=docker-compose.yml&type=
/api/packages/alice/composer/search.json?q=tools&type=
/api/packages/alice/conan/v1/conans/search?q=python3
/api/packages/alice/conan/v1/conans/search?q=powershell
/api/packages/alice/conan/v1/conans/search?q=ls+-la
/api/packages/alice/conan/v1/conans/search?q=.htaccess
/api/packages/alice/conan/v1/conans/search?q=%2Fbin%2Fsh
/api/packages/alice/conan/v1/conans/search?q=tools%2F%2A
/api/packages/alice/conan/v1/conans/search?q=docker-compose.yml
/api/packages/alice/conan/v1/conans/search?q=tools
/api/packages/alice/conan/v2/conans/search?q=python3
/api/packages/alice/conan/v2/conans/search?q=powershell
/api/packages/alice/conan/v2/conans/search?q=ls+-la
/api/packages/alice/conan/v2/conans/search?q=.htaccess
/api/packages/alice/conan/v2/conans/search?q=%2Fbin%2Fsh
/api/packages/alice/conan/v2/conans/search?q=tools%2F1.0%40alice%2Fstable
/api/packages/alice/conan/v2/conans/search?q=docker-compose.yml
/api/packages/alice/conan/v2/conans/search?q=tools
/api/packages/alice/nuget/query?q=python3&skip=0&take=20&prerelease=false&semVerLevel=2.0.0
/api/packages/alice/nuget/query?q=powershell&skip=0&take=20&prerelease=false&semVerLevel=2.0.0
/api/packages/alice/nuget/query?q=ls%20-la&skip=0&take=20&prerelease=false&semVerLevel=2.0.0
/api/packages/alice/nuget/query?q=.htaccess&skip=0&take=20&prerelease=false&semVerLevel=2.0.0
/api/packages/alice/nuget/query?q=docker-compose.yml&skip=0&take=20&prerelease=false&semVerLevel=2.0.0
/api/packages/alice/nuget/query?q=tools&skip=0&take=20&prerelease=false&semVerLevel=2.0.0
/api/packages/alice/nuget/Search()?$filter=IsLatestVersion&searchTerm='python3'&targetFramework=''&includePrerelease=false&$skip=0&$top=20&semVerLevel=2.0.0
/api/packages/alice/nuget/Search()?$filter=IsLatestVersion&searchTerm='powershell'&targetFramework=''&includePrerelease=false&$skip=0&$top=20&semVerLevel=2.0.0
/api/packages/alice/nuget/Search()?$filter=IsLatestVersion&searchTerm='ls%20-la'&targetFramework=''&includePrerelease=false&$skip=0&$top=20&semVerLevel=2.0.0
/api/packages/alice/nuget/Search()?$filter=IsLatestVersion&searchTerm='.htaccess'&targetFramework=''&includePrerelease=false&$skip=0&$top=20&semVerLevel=2.0.0
/api/packages/alice/nuget/Search()?$filter=IsLatestVersion&searchTerm='docker-compose.yml'&targetFramework=''&includePrerelease=false&$skip=0&$top=20&semVerLevel=2.0.0
/api/packages/alice/nuget/Search()?$filter=IsLatestVersion&searchTerm='tools'&targetFramework=''&includePrerelease=false&$skip=0&$top=20&semVerLevel=2.0.0
/api/packages/alice/nuget/Packages()?$filter=((((Id%20ne%20null)%20and%20substringof('python3',tolower(Id)))%20or%20((Description%20ne%20null)%20and%20substringof('python3',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20python3%20',tolower(Tags))))%20and%20IsLatestVersion&$orderby=Id&$skip=0&$top=30&semVerLevel=2.0.0
/api/packages/alice/nuget/Packages()?$filter=((((Id%20ne%20null)%20and%20substringof('powershell',tolower(Id)))%20or%20((Description%20ne%20null)%20and%20substringof('powershell',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20powershell%20',tolower(Tags))))%20and%20IsLatestVersion&$orderby=Id&$skip=0&$top=30&semVerLevel=2.0.0
/api/packages/alice/nuget/Packages()?$filter=(((((((Id%20ne%20null)%20and%20substringof('ls',tolower(Id)))%20or%20((Description%20ne%20null)%20and%20substringof('ls',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20ls%20',tolower(Tags))))%20or%20((Id%20ne%20null)%20and%20substringof('-la',tolower(Id))))%20or%20((Description%20ne%20null)%20and%20substringof('-la',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20-la%20',tolower(Tags))))%20and%20IsLatestVersion&$orderby=Id&$skip=0&$top=30&semVerLevel=2.0.0
/api/packages/alice/nuget/Packages()?$filter=((((Id%20ne%20null)%20and%20substringof('.htaccess',tolower(Id)))%20or%20((Description%20ne%20null)%20and%20substringof('.htaccess',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20.htaccess%20',tolower(Tags))))%20and%20IsLatestVersion&$orderby=Id&$skip=0&$top=30&semVerLevel=2.0.0
/api/packages/alice/nuget/Packages()?$filter=((((Id%20ne%20null)%20and%20substringof('docker-compose.yml',tolower(Id)))%20or%20((Description%20ne%20null)%20and%20substringof('docker-compose.yml',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20docker-compose.yml%20',tolower(Tags))))%20and%20IsLatestVersion&$orderby=Id&$skip=0&$top=30&semVerLevel=2.0.0
/api/packages/alice/nuget/Packages()?$filter=((((Id%20ne%20null)%20and%20substringof('tools',tolower(Id)))%20or%20((Description%20ne%20null)%20and%20substringof('tools',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20tools%20',tolower(Tags))))%20and%20IsLatestVersion&$orderby=Id&$skip=0&$top=30&semVerLevel=2.0.0
/api/packages/alice/nuget/Packages()?$filter=tolower(Id)%20eq%20'python3'&$top=30
/api/packages/alice/nuget/Packages()?$filter=tolower(Id)%20eq%20'powershell'&$top=30
/api/packages/alice/nuget/Packages()?$filter=tolower(Id)%20eq%20'.htaccess'&$top=30
/api/packages/alice/nuget/Packages()?$filter=tolower(Id)%20eq%20'docker-compose.yml'&$top=30
/api/packages/alice/nuget/Packages()?$filter=tolower(Id)%20eq%20'tools'&$top=30
/api/packages/alice/nuget/Search()/$count?$filter=IsLatestVersion&searchTerm='python3'&targetFramework=''&includePrerelease=false&$skip=0&$top=20&semVerLevel=2.0.0
/api/packages/alice/nuget/Search()/$count?$filter=IsLatestVersion&searchTerm='powershell'&targetFramework=''&includePrerelease=false&$skip=0&$top=20&semVerLevel=2.0.0
/api/packages/alice/nuget/Search()/$count?$filter=IsLatestVersion&searchTerm='ls%20-la'&targetFramework=''&includePrerelease=false&$skip=0&$top=20&semVerLevel=2.0.0
/api/packages/alice/nuget/Search()/$count?$filter=IsLatestVersion&searchTerm='.htaccess'&targetFramework=''&includePrerelease=false&$skip=0&$top=20&semVerLevel=2.0.0
/api/packages/alice/nuget/Search()/$count?$filter=IsLatestVersion&searchTerm='docker-compose.yml'&targetFramework=''&includePrerelease=false&$skip=0&$top=20&semVerLevel=2.0.0
/api/packages/alice/nuget/Search()/$count?$filter=IsLatestVersion&searchTerm='tools'&targetFramework=''&includePrerelease=false&$skip=0&$top=20&semVerLevel=2.0.0
/api/packages/alice/nuget/Packages()/$count?$filter=((((Id%20ne%20null)%20and%20substringof('python3',tolower(Id)))%20or%20((Description%20ne%20null)%20and%20substringof('python3',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20python3%20',tolower(Tags))))%20and%20IsLatestVersion&$orderby=Id&$skip=0&$top=30&semVerLevel=2.0.0
/api/packages/alice/nuget/Packages()/$count?$filter=((((Id%20ne%20null)%20and%20substringof('powershell',tolower(Id)))%20or%20((Description%20ne%20null)%20and%20substringof('powershell',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20powershell%20',tolower(Tags))))%20and%20IsLatestVersion&$orderby=Id&$skip=0&$top=30&semVerLevel=2.0.0
/api/packages/alice/nuget/Packages()/$count?$filter=(((((((Id%20ne%20null)%20and%20substringof('ls',tolower(Id)))%20or%20((Description%20ne%20null)%20and%20substringof('ls',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20ls%20',tolower(Tags))))%20or%20((Id%20ne%20null)%20and%20substringof('-la',tolower(Id))))%20or%20((Description%20ne%20null)%20and%20substringof('-la',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20-la%20',tolower(Tags))))%20and%20IsLatestVersion&$orderby=Id&$skip=0&$top=30&semVerLevel=2.0.0
/api/packages/alice/nuget/Packages()/$count?$filter=((((Id%20ne%20null)%20and%20substringof('.htaccess',tolower(Id)))%20or%20((Description%20ne%20null)%20and%20substringof('.htaccess',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20.htaccess%20',tolower(Tags))))%20and%20IsLatestVersion&$orderby=Id&$skip=0&$top=30&semVerLevel=2.0.0
/api/packages/alice/nuget/Packages()/$count?$filter=((((Id%20ne%20null)%20and%20substringof('docker-compose.yml',tolower(Id)))%20or%20((Description%20ne%20null)%20and%20substringof('docker-compose.yml',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20docker-compose.yml%20',tolower(Tags))))%20and%20IsLatestVersion&$orderby=Id&$skip=0&$top=30&semVerLevel=2.0.0
/api/packages/alice/nuget/Packages()/$count?$filter=((((Id%20ne%20null)%20and%20substringof('tools',tolower(Id)))%20or%20((Description%20ne%20null)%20and%20substringof('tools',tolower(Description))))%20or%20((Tags%20ne%20null)%20and%20substringof('%20tools%20',tolower(Tags))))%20and%20IsLatestVersion&$orderby=Id&$skip=0&$top=30&semVerLevel=2.0.0
/api/packages/alice/nuget/Packages()/$count?$filter=tolower(Id)%20eq%20'python3'&$top=30
/api/packages/alice/nuget/Packages()/$count?$filter=tolower(Id)%20eq%20'powershell'&$top=30
/api/packages/alice/nuget/Packages()/$count?$filter=tolower(Id)%20eq%20'.htaccess'&$top=30
/api/packages/alice/nuget/Packages()/$count?$filter=tolower(Id)%20eq%20'docker-compose.yml'&$top=30
/api/packages/alice/nuget/Packages()/$count?$filter=tolower(Id)%20eq%20'tools'&$top=30
/api/packages/alice/npm/-/v1/search?text=python3&size=20&from=0&quality=0.65&popularity=0.98&maintenance=0.5
/api/packages/alice/npm/-/v1/search?text=powershell&size=20&from=0&quality=0.65&popularity=0.98&maintenance=0.5
/api/packages/alice/npm/-/v1/search?text=ls+-la&size=20&from=0&quality=0.65&popularity=0.98&maintenance=0.5
/api/packages/alice/npm/-/v1/search?text=.htaccess&size=20&from=0&quality=0.65&popularity=0.98&maintenance=0.5
/api/packages/alice/npm/-/v1/search?text=%2Fbin%2Fsh&size=20&from=0&quality=0.65&popularity=0.98&maintenance=0.5
/api/packages/alice/npm/-/v1/search?text=%40alice%2Ftools-cli&size=20&from=0&quality=0.65&popularity=0.98&maintenance=0.5
/api/packages/alice/npm/-/v1/search?text=docker-compose.yml&size=20&from=0&quality=0.65&popularity=0.98&maintenance=0.5
/api/packages/alice/npm/-/v1/search?text=tools&size=20&from=0&quality=0.65&popularity=0.98&maintenance=0.5
# Pages, raw files and edit pages of files, and the API's call for their # Pages, raw files and edit pages of files, and the API's call for their
# contents. # contents.
/owner/repo/src/branch/main/package.json /owner/repo/src/branch/main/package.json
+2 -7
View File
@@ -21,12 +21,7 @@ const (
// the app. // the app.
ActionForward = "forward" ActionForward = "forward"
// ActionRequestLineTooLong is a request whose request line is over the // ActionRequestHeaderTooLarge is a request over the header limits.
// header limits' longest line.
ActionRequestLineTooLong = "request_line_too_long"
// ActionRequestHeaderTooLarge is a request whose headers are over the
// header limits.
ActionRequestHeaderTooLarge = "request_header_too_large" ActionRequestHeaderTooLarge = "request_header_too_large"
// ActionClientIsRateLimited is a request refused because it took its // ActionClientIsRateLimited is a request refused because it took its
@@ -149,7 +144,7 @@ type Line struct {
// taken with a request it would have refused: ActionClientDenied, // taken with a request it would have refused: ActionClientDenied,
// ActionTemporarilyBanned, ActionPermanentlyBanned, ActionCountryDenied, // ActionTemporarilyBanned, ActionPermanentlyBanned, ActionCountryDenied,
// ActionClientIsRateLimited, ActionBlockedByBlockRule, // ActionClientIsRateLimited, ActionBlockedByBlockRule,
// ActionBlockedByCoreRuleSet, or, in waf, ActionRequestLineTooLong or // ActionBlockedByCoreRuleSet, or, in waf,
// ActionRequestHeaderTooLarge for the header limits. // ActionRequestHeaderTooLarge for the header limits.
WouldAction string `json:"would_action,omitempty"` WouldAction string `json:"would_action,omitempty"`
// Heuristic is the name of the heuristic that refused the request, in // Heuristic is the name of the heuristic that refused the request, in
+1 -110
View File
@@ -39,7 +39,6 @@ const (
// The header limits SPEC.md gives. // The header limits SPEC.md gives.
cookieLimit = 50 cookieLimit = 50
headerFieldLimit = 100 headerFieldLimit = 100
lineLimit = 8192
// cookieHeader is the header the cookie limit counts. // cookieHeader is the header the cookie limit counts.
cookieHeader = "Cookie" cookieHeader = "Cookie"
// tooLarge is how waf refuses a request over the header limits. // tooLarge is how waf refuses a request over the header limits.
@@ -108,119 +107,11 @@ func TestHeaderLimitRefusalNamesItsHeuristicCountsAndLimits(t *testing.T) {
fillHeaders(req, headerFieldLimit+1) fillHeaders(req, headerFieldLimit+1)
do(t, req) do(t, req)
// The longest header line is X-Request-ID's, with its 26-character id.
wantHeuristic( wantHeuristic(
t, t,
s.out.requestLine(t), s.out.requestLine(t),
"header_limits", "header_limits",
`{"longest_header_line_bytes": 40, "header_line_bytes_limit": 8192, `{"fields": 101, "fields_limit": 100, "cookies": 0, "cookies_limit": 50}`,
"fields": 101, "fields_limit": 100, "cookies": 0, "cookies_limit": 50}`,
)
}
// longQuery returns a path whose request line, sent by Go's client as
// "GET <path> HTTP/1.1", is lineBytes long.
func longQuery(lineBytes int) string {
const around = len("GET /?q= HTTP/1.1")
return "/?q=" + strings.Repeat("a", lineBytes-around)
}
func TestARequestLineOverTheLimitIsRefusedWith414BeforeAnyOtherCheck(
t *testing.T,
) {
t.Parallel()
for _, tc := range []struct {
name string
lineBytes int
status int
action string
}{
{"at the limit", lineLimit, http.StatusOK, requestlog.ActionForward},
{
"over the limit", lineLimit + 1, http.StatusRequestURITooLong,
requestlog.ActionRequestLineTooLong,
},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
s := startWAF(t, "", map[string]string{coreRuleSetMode: block})
got := do(t, s.question(siteA, client, longQuery(tc.lineBytes)))
line := s.out.requestLine(t)
if tc.status == http.StatusOK {
wantDisposition(t, got, tc.status, waf.DispositionAllow)
} else {
wantDisposition(t, got, tc.status, waf.DispositionDeny)
}
wantLine(t, line, tc.status, tc.action)
refused := tc.status != http.StatusOK
if refused != (line.DurationCoreRuleSetMS == nil) {
t.Errorf(
"Core Rule Set time %v for a request refused: %t",
line.DurationCoreRuleSetMS,
refused,
)
}
})
}
}
func TestAHeaderLineOverTheLimitIsRefusedWith431(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
make func(*http.Request)
// refused is true for a request over the limit, and false for one at it.
refused bool
}{
{"a header line at the limit", func(req *http.Request) {
req.Header.Set("X-Long", strings.Repeat("a", lineLimit-len("X-Long: ")))
}, false},
{"a header line over the limit", func(req *http.Request) {
req.Header.Set("X-Long", strings.Repeat("a", lineLimit+1-len("X-Long: ")))
}, true},
{"one cookie over the limit", func(req *http.Request) {
req.Header.Set(cookieHeader, "a="+strings.Repeat("a", lineLimit))
}, true},
{"a Host line over the limit", func(req *http.Request) {
req.Host = strings.Repeat("a", lineLimit+1-len("Host: "))
}, true},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
s := startWAF(t, "", nil)
req := s.question(siteA, client, "/")
tc.make(req)
got := do(t, req)
if tc.refused {
wantDisposition(t, got, tooLarge, waf.DispositionDeny)
} else {
wantDisposition(t, got, http.StatusOK, waf.DispositionAllow)
}
})
}
}
func TestRequestLineRefusalNamesItsHeuristicLengthAndLimit(t *testing.T) {
t.Parallel()
s := startWAF(t, "", nil)
do(t, s.question(siteA, client, longQuery(lineLimit+1)))
wantHeuristic(
t,
s.out.requestLine(t),
"header_limits",
`{"request_line_bytes": 8193, "request_line_bytes_limit": 8192}`,
) )
} }
+7 -52
View File
@@ -10,22 +10,16 @@ import (
"sneak.berlin/go/smallwebwaf/internal/requestlog" "sneak.berlin/go/smallwebwaf/internal/requestlog"
) )
// The header limits: the most cookies and header fields a request may have, // The header limits: the most cookies and header fields a request may have.
// and the longest its request line and each header line may be, in bytes, not // Real clients never reach them.
// counting the line ending. Real clients never reach them. maxLineBytes is
// what nginx allows by default.
const ( const (
maxCookies = 50 maxCookies = 50
maxHeaderFields = 100 maxHeaderFields = 100
maxLineBytes = 8 << 10
) )
// headerLimits refuses a request with a request line longer than maxLineBytes, // headerLimits refuses a request with more than maxHeaderFields header fields
// with 414. It refuses a request with a header line longer than maxLineBytes, // or more than maxCookies cookies, with 431. Its figures are the request's
// more than maxHeaderFields header fields or more than maxCookies cookies, with // header fields and cookies, and the limit of each.
// 431. Its figures are the request line's length and its limit for 414. For
// 431 they are the longest header line, the header fields and the cookies,
// each with its limit.
type headerLimits struct{} type headerLimits struct{}
func (headerLimits) name() string { return "header_limits" } func (headerLimits) name() string { return "header_limits" }
@@ -35,59 +29,20 @@ func (headerLimits) check(
rq *request, rq *request,
_ time.Time, _ time.Time,
) *verdict { ) *verdict {
requestLine := rq.requestLineBytes()
if requestLine > maxLineBytes {
return &verdict{
action: requestlog.ActionRequestLineTooLong,
figures: map[string]any{
"request_line_bytes": requestLine,
"request_line_bytes_limit": maxLineBytes,
},
}
}
longest := rq.longestHeaderLineBytes()
fields, cookies := rq.headerCounts() fields, cookies := rq.headerCounts()
if fields <= maxHeaderFields && cookies <= maxCookies {
if longest <= maxLineBytes && fields <= maxHeaderFields &&
cookies <= maxCookies {
return nil return nil
} }
return &verdict{ return &verdict{
action: requestlog.ActionRequestHeaderTooLarge, action: requestlog.ActionRequestHeaderTooLarge,
figures: map[string]any{ figures: map[string]any{
"longest_header_line_bytes": longest, "fields": fields, "fields_limit": maxHeaderFields,
"header_line_bytes_limit": maxLineBytes,
"fields": fields, "fields_limit": maxHeaderFields,
"cookies": cookies, "cookies_limit": maxCookies, "cookies": cookies, "cookies_limit": maxCookies,
}, },
} }
} }
// requestLineBytes returns the length of the request's request line, such as
// "GET /search?q=x HTTP/1.1", not counting its line ending. The target is the
// one the visitor sent, which rproxy's question carries unchanged.
func (rq *request) requestLineBytes() int {
return len(rq.in.Method) + len(" ") + len(rq.in.RequestURI) +
len(" ") + len(rq.in.Proto)
}
// longestHeaderLineBytes returns the length of the request's longest header
// line, such as "Cookie: a=1", not counting its line ending. Each value of a
// header sent twice is a line of its own, and Host is a line too.
func (rq *request) longestHeaderLineBytes() int {
longest := len("Host: ") + len(rq.in.Host)
for name, values := range rq.in.Header {
for _, value := range values {
longest = max(longest, len(name)+len(": ")+len(value))
}
}
return longest
}
// headerCounts returns the request's header fields, each value of a header sent // headerCounts returns the request's header fields, each value of a header sent
// twice being one, and its cookies, each part of a Cookie header between // twice being one, and its cookies, each part of a Cookie header between
// semicolons being one unless it is empty or only spaces and tabs. The Core // semicolons being one unless it is empty or only spaces and tabs. The Core
+2 -2
View File
@@ -41,8 +41,8 @@ const (
// beforeLookup are the heuristics that run before the client's AS number and // beforeLookup are the heuristics that run before the client's AS number and
// country are looked up, so that a client they refuse is not looked up. // country are looked up, so that a client they refuse is not looked up.
// header_limits comes first: it only measures and counts, and a request it // header_limits comes first: it only counts, and a request it refuses causes no
// refuses causes no other work. // other work.
func beforeLookup() []heuristic { func beforeLookup() []heuristic {
return []heuristic{ return []heuristic{
headerLimits{}, headerLimits{},
+1 -3
View File
@@ -177,7 +177,7 @@ func requestHeaders(r *http.Request, names []string) map[string]string {
// check is the one place where a request can be refused. It returns nil to let // check is the one place where a request can be refused. It returns nil to let
// the request through. The heuristics of checkClient decide: a request they // the request through. The heuristics of checkClient decide: a request they
// refuse is answered with SWW_WAF_BAN_RESPONSE, 403 for a block rule or the Core // refuse is answered with SWW_WAF_BAN_RESPONSE, 403 for a block rule or the Core
// Rule Set, or 414 or 431 for the header limits. In observe mode nothing is refused, // Rule Set, or 431 for the header limits. In observe mode nothing is refused,
// and the log line names what enforce mode would have done. ctx is the // and the log line names what enforce mode would have done. ctx is the
// request's own context. // request's own context.
func (rq *request) check(ctx context.Context) *refusal { func (rq *request) check(ctx context.Context) *refusal {
@@ -196,8 +196,6 @@ func (rq *request) check(ctx context.Context) *refusal {
case action == requestlog.ActionBlockedByBlockRule || case action == requestlog.ActionBlockedByBlockRule ||
action == requestlog.ActionBlockedByCoreRuleSet: action == requestlog.ActionBlockedByCoreRuleSet:
return &refusal{status: http.StatusForbidden, action: action} return &refusal{status: http.StatusForbidden, action: action}
case action == requestlog.ActionRequestLineTooLong:
return &refusal{status: http.StatusRequestURITooLong, action: action}
case action == requestlog.ActionRequestHeaderTooLarge: case action == requestlog.ActionRequestHeaderTooLarge:
return &refusal{ return &refusal{
status: http.StatusRequestHeaderFieldsTooLarge, status: http.StatusRequestHeaderFieldsTooLarge,
+1 -6
View File
@@ -1690,12 +1690,7 @@ func TestALargeBodyAnnouncedAndLongHeadersAreNoReasonToRefuse(t *testing.T) {
strings.NewReader("a body")) strings.NewReader("a body"))
req.Header.Set(waf.ContentLengthHeader, "1073741824") req.Header.Set(waf.ContentLengthHeader, "1073741824")
req.Header.Set(forwardedFor, otherClient) req.Header.Set(forwardedFor, otherClient)
req.Header.Set("X-Long", strings.Repeat("x", 40<<10))
// 40,000 bytes of headers, over rproxy's 32 KiB, in lines within the
// header limits.
for i := range 5 {
req.Header.Set("X-Long-"+strconv.Itoa(i), strings.Repeat("x", 8000))
}
got := do(t, req) got := do(t, req)
wantDisposition(t, got, http.StatusOK, waf.DispositionAllow) wantDisposition(t, got, http.StatusOK, waf.DispositionAllow)