Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f2699c591 | ||
|
|
50c50bfcae | ||
|
|
b84a33fb57 |
No files matched your search
@@ -543,9 +543,10 @@ request reaches the app.
|
||||
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
|
||||
false positives; such refusals count toward `SWW_WAF_REFUSALS_LIMIT_PER_MINUTE`
|
||||
instead. A request over the header limits of step 3 is answered `431`, and bans
|
||||
nobody either. `rproxy` itself answers `405` to a method it does not pass on,
|
||||
and the status for a size or time limit, such as `413` for a body too large.
|
||||
instead. A request over the header limits of step 3 is answered `414` for a
|
||||
request line over 8 KiB and `431` otherwise. It bans nobody either. `rproxy`
|
||||
itself answers `405` to a method it does not pass on, and the status for a size
|
||||
or time limit, such as `413` for a body too large.
|
||||
|
||||
### Clients and bans
|
||||
|
||||
|
||||
@@ -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
|
||||
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
|
||||
after `SWW_WAF_ALLOW_NETS`, since they only count header fields and cookies.
|
||||
Next, the tag lists (`tag_lists`; see "Request tags"). Each list that holds
|
||||
the client adds its tag. Next, `routewatch_tags` adds the client's `asn=` and
|
||||
`prefix=` tags while `SWW_WAF_ROUTEWATCH_URL` is set. A client in
|
||||
`SWW_WAF_ALLOW_NETS` skips the header limits and gets no tag.
|
||||
after `SWW_WAF_ALLOW_NETS`, since they only measure lines and count header
|
||||
fields and cookies. Next, the tag lists (`tag_lists`; see "Request tags").
|
||||
Each list that holds the client adds its tag. Next, `routewatch_tags` adds
|
||||
the client's `asn=` and `prefix=` tags while `SWW_WAF_ROUTEWATCH_URL` is set.
|
||||
A client in `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
|
||||
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.
|
||||
@@ -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
|
||||
ban's notes.
|
||||
|
||||
A request over the header limits is refused with `431` before the bans are
|
||||
checked (see "Answering in time"). It makes no ban, makes no ban permanent and
|
||||
is not counted in a ban's notes.
|
||||
A request over the header limits is refused with `414` or `431` before the bans
|
||||
are checked (see "Answering in time"). It makes no ban, makes no ban permanent
|
||||
and is not counted in a ban's notes.
|
||||
|
||||
An offence is a request `waf` holds against the client. It is one of:
|
||||
|
||||
@@ -888,7 +888,8 @@ one entry of `apps` in `internal/coreruleset/coreruleset.go`. gitea's fixes are:
|
||||
- a repository's branches, tags and packages pages;
|
||||
- an owner's packages page;
|
||||
- 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
|
||||
`python3`, `powershell`, `ls -la` and `.htaccess` were refused there. The
|
||||
@@ -905,7 +906,9 @@ one entry of `apps` in `internal/coreruleset/coreruleset.go`. gitea's fixes are:
|
||||
- the admin lists of users, organisations, repositories, unadopted
|
||||
repositories and emails;
|
||||
- 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
|
||||
repository or activate an email send it in the body. gitea serves its
|
||||
@@ -916,6 +919,31 @@ one entry of `apps` in `internal/coreruleset/coreruleset.go`. gitea's fixes are:
|
||||
and a name, such as `alice/python3-app`;
|
||||
- `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:
|
||||
- a file's page;
|
||||
- its raw file;
|
||||
@@ -2375,7 +2403,7 @@ answer but the last below carries `X-Sww-Disposition`.
|
||||
of:
|
||||
- `SWW_WAF_BAN_RESPONSE`, for a refusal;
|
||||
- `403`, for a `block` rule or the Core Rule Set;
|
||||
- `431`, for a request over the header limits;
|
||||
- `414` or `431`, for a request over the header limits;
|
||||
- `404`, for a path under `/_sww/` that passes the checks, so that the app
|
||||
never sees that prefix.
|
||||
- `X-Sww-Disposition: close`, status `200`, no body. This takes the place of a
|
||||
@@ -2437,22 +2465,31 @@ never wait. These can still wait for GeoJS:
|
||||
- a rule file;
|
||||
- the Core Rule Set.
|
||||
|
||||
The header limits are fixed, with no setting. `waf` refuses a request over
|
||||
either one:
|
||||
The header limits are fixed, with no setting. A line's length is in bytes and
|
||||
does not count its line ending. `waf` refuses a request whose request line is
|
||||
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
|
||||
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
|
||||
`Transfer-Encoding` do not count. `X-Request-ID` and `X-Forwarded-Proto` do.
|
||||
|
||||
Real clients never reach these limits. The answer is `deny` with `431`, and the
|
||||
log line's action is `request_header_too_large`. The refusal makes no ban and is
|
||||
no offence.
|
||||
Real clients never reach these limits. nginx refuses lines over 8 KiB by
|
||||
default, and Apache's default limit is about the same. The answer is `deny`. The
|
||||
log line's action is `request_line_too_long` for `414` and
|
||||
`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
|
||||
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
|
||||
client under a ban or in `SWW_WAF_DENY_NETS` also gets `431` for such a request.
|
||||
client under a ban or in `SWW_WAF_DENY_NETS` also gets `414` or `431` for such a
|
||||
request.
|
||||
|
||||
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
|
||||
@@ -2460,10 +2497,10 @@ client in `SWW_WAF_ALLOW_NETS` skips them, as it skips the Core Rule Set. In
|
||||
other check runs on it.
|
||||
|
||||
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. Within the limits, one long cookie
|
||||
or query string can still keep it busy for a fraction of a second. So enough
|
||||
such requests at once can overload `waf` past
|
||||
`SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS`.
|
||||
cookies would keep it busy for over a second. One 30 KB cookie or query string
|
||||
would keep it busy for a third of a second or more. Within the limits, a request
|
||||
takes less of its time. Enough such requests at once can still overload `waf`
|
||||
past `SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS`.
|
||||
|
||||
### The report
|
||||
|
||||
|
||||
+6
-2
@@ -102,7 +102,8 @@ Every one of these lines has:
|
||||
ban this request made or made permanent;
|
||||
- `country_denied`, `client_is_rate_limited`, `blocked_by_block_rule` and
|
||||
`blocked_by_core_rule_set`: refused by that check;
|
||||
- `request_header_too_large`: over a header limit;
|
||||
- `request_line_too_long`: a request line over the header limits;
|
||||
- `request_header_too_large`: headers over the header limits;
|
||||
- `own_endpoint`: answered at one of `waf`'s own endpoints, or `404` for a
|
||||
path under `/_sww/`.
|
||||
- `would_action`: in `observe` mode, what `enforce` mode would have done with a
|
||||
@@ -126,7 +127,10 @@ Every one of these lines has:
|
||||
- `rate_limits`: `window`, `count` and `limit`;
|
||||
- `trap_paths`: `path`, the trap path asked for;
|
||||
- `rule_files`: `rule_id` and `rule_action`, of the rule that refused it;
|
||||
- `header_limits`: `fields`, `fields_limit`, `cookies` and `cookies_limit`;
|
||||
- `header_limits`: for a request line, `request_line_bytes` and
|
||||
`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`,
|
||||
`SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD`;
|
||||
- `refusal_limit`: `window`, `count` and `limit`, of the refusal limit;
|
||||
|
||||
+3
-2
@@ -432,8 +432,9 @@ A few limits are fixed rather than settings:
|
||||
- at most 20,000 anomaly counters are kept;
|
||||
- each alert destination's queue holds at most 1,000 alerts;
|
||||
- `waf` holds at most 10,000 answered requests awaiting their reports;
|
||||
- `waf` refuses a request with more than 50 cookies or more than 100 header
|
||||
fields (see "Answering in time" under "`waf`" in [`SPEC.md`](../SPEC.md));
|
||||
- `waf` refuses a request with a request line or header line over 8 KiB, more
|
||||
than 50 cookies or more than 100 header fields (see "Answering in time" under
|
||||
"`waf`" in [`SPEC.md`](../SPEC.md));
|
||||
- `rproxy` takes a request line and headers of at most 32 KiB, and closes a
|
||||
kept-open connection idle for 120 seconds;
|
||||
- `rproxy` reads at most 1 MiB of the body of `waf`'s answer, and answers `502`
|
||||
|
||||
@@ -172,11 +172,13 @@ SecRule REQUEST_FILENAME "@rx ^/(?:explore/repos|user/search_candidates|\
|
||||
|
||||
# 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
|
||||
# pages, on an owner's packages page, on the admin page of packages and on the
|
||||
# API's search of packages also skips this rule:
|
||||
# pages, on an owner's packages page, on the admin page of packages, on the
|
||||
# API's search of packages and on the Composer and Conan registries' searches
|
||||
# also skips this rule:
|
||||
# 932160: /bin/sh
|
||||
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"
|
||||
|
||||
# gitea's other searches send a name or a few words in q too, which skips only
|
||||
@@ -191,6 +193,8 @@ SecRule REQUEST_FILENAME "@rx ^/(?:[^/]+/[^/]+/(?:branches|tags|packages)|\
|
||||
# - the lists of users and organisations;
|
||||
# - the API's searches of users, of topics, of an organisation's teams and of
|
||||
# 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;
|
||||
# - 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
|
||||
@@ -210,6 +214,8 @@ SecRule REQUEST_FILENAME "@rx ^/(?:repo/search|notifications/watching|\
|
||||
explore/(?:users|organizations)|\
|
||||
api/v1/(?:users/search|topics/search|orgs/[^/]+/teams/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)|\
|
||||
[^/]+/-/packages/[^/]+/.+/versions|\
|
||||
[^/]+/[^/]+/(?:(?:issues|pulls)/posters|issues/suggestions|milestones|\
|
||||
@@ -246,6 +252,76 @@ SecRule REQUEST_FILENAME "@rx ^/api/v1/repos/[^/]+/[^/]+/milestones$" \
|
||||
ctl:ruleRemoveTargetById=932250;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
|
||||
# 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:
|
||||
|
||||
@@ -271,15 +271,28 @@ const giteaAPIPackageSearch = "/api/v1/packages/alice?q="
|
||||
func TestGiteaAttacksAreRefusedInItsAPISearches(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const threshold = 5 // SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default
|
||||
|
||||
defaults, bodies := atDefaults(t), readingBodies(t)
|
||||
searches := slices.Concat(
|
||||
checkSearchesRefuseAttacks(
|
||||
t,
|
||||
giteaAPISearches,
|
||||
[]string{giteaAPIPackageSearch},
|
||||
)
|
||||
}
|
||||
|
||||
for _, search := range searches {
|
||||
// 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
|
||||
|
||||
defaults, bodies := atDefaults(t), readingBodies(t)
|
||||
|
||||
for _, search := range slices.Concat(refusingBinSh, lettingBinSh) {
|
||||
path, field, _ := strings.Cut(search, "?")
|
||||
|
||||
for _, attack := range []string{
|
||||
@@ -303,8 +316,7 @@ func TestGiteaAttacksAreRefusedInItsAPISearches(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// /bin/sh stays refused on the searches that are not of packages.
|
||||
for _, search := range giteaAPISearches {
|
||||
for _, search := range refusingBinSh {
|
||||
if got := inspect(t, defaults, get(search+binSh)); got.Score < threshold {
|
||||
t.Errorf("%s%s let through: %+v", search, binSh, got)
|
||||
}
|
||||
@@ -314,14 +326,9 @@ func TestGiteaAttacksAreRefusedInItsAPISearches(t *testing.T) {
|
||||
func TestGiteaAPISearchExclusionsAreNarrow(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const threshold = 5 // SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD's default
|
||||
|
||||
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{
|
||||
// Each search's field on paths near the searches, and another field on
|
||||
// them.
|
||||
checkValuesStayRefused(t, []string{
|
||||
"/api/v1/topics/search/x?q=",
|
||||
"/api/v1/orgs/acme/teams?q=",
|
||||
"/api/v1/orgs/acme/teams/search/x?q=",
|
||||
@@ -337,7 +344,19 @@ func TestGiteaAPISearchExclusionsAreNarrow(t *testing.T) {
|
||||
"/api/v1/admin/unadopted?q=",
|
||||
"/api/v1/repos/alice/tools/milestones?q=",
|
||||
"/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(
|
||||
[]string{binSh, commandInjection, pathTraversal},
|
||||
hardValues,
|
||||
@@ -348,3 +367,135 @@ func TestGiteaAPISearchExclusionsAreNarrow(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
@@ -1034,3 +1034,103 @@
|
||||
{"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=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
@@ -442,6 +442,106 @@
|
||||
/api/v1/repos/alice/tools/milestones?state=open&name=.htaccess
|
||||
/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
|
||||
# contents.
|
||||
/owner/repo/src/branch/main/package.json
|
||||
|
||||
@@ -21,7 +21,12 @@ const (
|
||||
// the app.
|
||||
ActionForward = "forward"
|
||||
|
||||
// ActionRequestHeaderTooLarge is a request over the header limits.
|
||||
// ActionRequestLineTooLong is a request whose request line is over the
|
||||
// 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"
|
||||
|
||||
// ActionClientIsRateLimited is a request refused because it took its
|
||||
@@ -144,7 +149,7 @@ type Line struct {
|
||||
// taken with a request it would have refused: ActionClientDenied,
|
||||
// ActionTemporarilyBanned, ActionPermanentlyBanned, ActionCountryDenied,
|
||||
// ActionClientIsRateLimited, ActionBlockedByBlockRule,
|
||||
// ActionBlockedByCoreRuleSet, or, in waf,
|
||||
// ActionBlockedByCoreRuleSet, or, in waf, ActionRequestLineTooLong or
|
||||
// ActionRequestHeaderTooLarge for the header limits.
|
||||
WouldAction string `json:"would_action,omitempty"`
|
||||
// Heuristic is the name of the heuristic that refused the request, in
|
||||
|
||||
@@ -39,6 +39,7 @@ const (
|
||||
// The header limits SPEC.md gives.
|
||||
cookieLimit = 50
|
||||
headerFieldLimit = 100
|
||||
lineLimit = 8192
|
||||
// cookieHeader is the header the cookie limit counts.
|
||||
cookieHeader = "Cookie"
|
||||
// tooLarge is how waf refuses a request over the header limits.
|
||||
@@ -107,11 +108,119 @@ func TestHeaderLimitRefusalNamesItsHeuristicCountsAndLimits(t *testing.T) {
|
||||
fillHeaders(req, headerFieldLimit+1)
|
||||
do(t, req)
|
||||
|
||||
// The longest header line is X-Request-ID's, with its 26-character id.
|
||||
wantHeuristic(
|
||||
t,
|
||||
s.out.requestLine(t),
|
||||
"header_limits",
|
||||
`{"fields": 101, "fields_limit": 100, "cookies": 0, "cookies_limit": 50}`,
|
||||
`{"longest_header_line_bytes": 40, "header_line_bytes_limit": 8192,
|
||||
"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}`,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,16 +10,22 @@ import (
|
||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||
)
|
||||
|
||||
// The header limits: the most cookies and header fields a request may have.
|
||||
// Real clients never reach them.
|
||||
// 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
|
||||
// counting the line ending. Real clients never reach them. maxLineBytes is
|
||||
// what nginx allows by default.
|
||||
const (
|
||||
maxCookies = 50
|
||||
maxHeaderFields = 100
|
||||
maxLineBytes = 8 << 10
|
||||
)
|
||||
|
||||
// headerLimits refuses a request with more than maxHeaderFields header fields
|
||||
// or more than maxCookies cookies, with 431. Its figures are the request's
|
||||
// header fields and cookies, and the limit of each.
|
||||
// headerLimits refuses a request with a request line longer than maxLineBytes,
|
||||
// with 414. It refuses a request with a header line longer than maxLineBytes,
|
||||
// more than maxHeaderFields header fields or more than maxCookies cookies, with
|
||||
// 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{}
|
||||
|
||||
func (headerLimits) name() string { return "header_limits" }
|
||||
@@ -29,20 +35,59 @@ func (headerLimits) check(
|
||||
rq *request,
|
||||
_ time.Time,
|
||||
) *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()
|
||||
if fields <= maxHeaderFields && cookies <= maxCookies {
|
||||
|
||||
if longest <= maxLineBytes && fields <= maxHeaderFields &&
|
||||
cookies <= maxCookies {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &verdict{
|
||||
action: requestlog.ActionRequestHeaderTooLarge,
|
||||
figures: map[string]any{
|
||||
"fields": fields, "fields_limit": maxHeaderFields,
|
||||
"longest_header_line_bytes": longest,
|
||||
"header_line_bytes_limit": maxLineBytes,
|
||||
"fields": fields, "fields_limit": maxHeaderFields,
|
||||
"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
|
||||
// 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
|
||||
|
||||
@@ -41,8 +41,8 @@ const (
|
||||
|
||||
// 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.
|
||||
// header_limits comes first: it only counts, and a request it refuses causes no
|
||||
// other work.
|
||||
// header_limits comes first: it only measures and counts, and a request it
|
||||
// refuses causes no other work.
|
||||
func beforeLookup() []heuristic {
|
||||
return []heuristic{
|
||||
headerLimits{},
|
||||
|
||||
@@ -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
|
||||
// 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
|
||||
// Rule Set, or 431 for the header limits. In observe mode nothing is refused,
|
||||
// Rule Set, or 414 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
|
||||
// request's own context.
|
||||
func (rq *request) check(ctx context.Context) *refusal {
|
||||
@@ -196,6 +196,8 @@ func (rq *request) check(ctx context.Context) *refusal {
|
||||
case action == requestlog.ActionBlockedByBlockRule ||
|
||||
action == requestlog.ActionBlockedByCoreRuleSet:
|
||||
return &refusal{status: http.StatusForbidden, action: action}
|
||||
case action == requestlog.ActionRequestLineTooLong:
|
||||
return &refusal{status: http.StatusRequestURITooLong, action: action}
|
||||
case action == requestlog.ActionRequestHeaderTooLarge:
|
||||
return &refusal{
|
||||
status: http.StatusRequestHeaderFieldsTooLarge,
|
||||
|
||||
@@ -1690,7 +1690,12 @@ func TestALargeBodyAnnouncedAndLongHeadersAreNoReasonToRefuse(t *testing.T) {
|
||||
strings.NewReader("a body"))
|
||||
req.Header.Set(waf.ContentLengthHeader, "1073741824")
|
||||
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)
|
||||
wantDisposition(t, got, http.StatusOK, waf.DispositionAllow)
|
||||
|
||||
Reference in new issue
Block a user