Gitea setup: Core Rule Set blocks attacks, not ordinary Gitea traffic (closes #198)
check / check (push) Successful in 3m16s

The Gitea setup runs the Core Rule Set in block mode. Rules that refused
ordinary Gitea requests are switched off only where they misfired:

- deploy/gitea/crs-exclusions.conf, loaded through the new setting
  SWWAF_CORE_RULE_SET_EXCLUSIONS_FILE before the Core Rule Set's rules,
  covers Gitea's search, file-name and branch-name fields, Referer, file
  paths, API attachment names and Actions artifact uploads;
- smallwebwaf's defaults gain a seventh change: the registry upload
  path's from parameter skips 932260.

A file that cannot be read, does not parse or names an id from 900000
to 900999 stops servewaf at start, in every mode. internal/waf's tests
and script/gitea-setup send the recorded requests in
internal/waf/testdata/gitea-requests.txt and three attacks.

Model: opus-5-5
This commit is contained in:
clawbot committed 2026-11-06 20:17:07 +00:00
1 parent 21cac70b50
commit 866b948ec0
17 files changed
+1048 -242

No files matched your search

+77
View File
@@ -5,6 +5,12 @@
# rproxy, and that servewaf checks them;
# - that gitea logs the visitor's address, not rproxy's;
# - that a request from a container on the shared network skips the checks;
# - that the Core Rule Set refuses none of the ordinary gitea requests in
# internal/waf/testdata/gitea-requests.txt;
# - that it refuses none of the API calls with JSON bodies;
# - that it refuses no fetch;
# - that it refuses SQL injection, path traversal and script injection in a
# query string;
# - that with servewaf stopped, gitea still answers through rproxy.
# It publishes no port of traefik's, since this host's ports 80 and 443 may be
# taken. It reaches traefik at its address on the shared network instead, so
@@ -136,6 +142,71 @@ passed_through() {
"\"client_ip\":\"$gateway\"" '"action":"forward"' '"counts":' '"waf_score":'
}
# refused: print each request servewaf logged as refused, for any reason.
refused() {
compose logs --no-color --no-log-prefix servewaf 2>&1 |
grep -F '"servewaf_line":"report"' | grep -v -F '"action":"forward"' || true
}
# ordinary_requests: sign in as the owner, save a file and open an issue
# through the API with JSON bodies, then send, signed in, each request in
# internal/waf/testdata/gitea-requests.txt, with the header its line ends with,
# if any, and that request's own address as its Referer, as a page opened from it
# has. One a second keeps them under the default limit of 120 requests a minute,
# since a request refused for a limit is never inspected. Fail if servewaf
# refuses any of them.
ordinary_requests() {
send /user/login --cookie-jar "$WORK/cookies" >/dev/null
status="$(send /user/login --cookie "$WORK/cookies" --cookie-jar "$WORK/cookies" \
--header "Origin: https://$SITE" --data-urlencode user_name=owner \
--data-urlencode "password=$PASSWORD")"
[ "$status" = 303 ] || fail "signing in was answered $status"
content="$(printf 'declare const env: Record<string, string>;\n' | base64)"
status="$(send /api/v1/repos/owner/repo/contents/process.env.d.ts \
--user "owner:$PASSWORD" --header "Content-Type: application/json" \
--data "{\"message\":\"Add process.env.d.ts\",\"content\":\"$content\"}")"
[ "$status" = 201 ] || fail "saving a file through the API was answered $status"
status="$(send /api/v1/repos/owner/repo/issues --user "owner:$PASSWORD" \
--header "Content-Type: application/json" \
--data '{"title":"ls -la fails","body":"SELECT * FROM users; <img src=x>"}')"
[ "$status" = 201 ] || fail "opening an issue through the API was answered $status"
grep -v -e '^#' -e '^$' "$ROOT/internal/waf/testdata/gitea-requests.txt" |
while read -r target header; do
if [ -n "$header" ]; then
send "$target" --globoff --cookie "$WORK/cookies" \
--header "Referer: https://$SITE$target" --header "$header" >/dev/null
else
send "$target" --globoff --cookie "$WORK/cookies" \
--header "Referer: https://$SITE$target" >/dev/null
fi
sleep 1
done
# The last request's report is logged once its answer has ended.
send /api/v1/settings/api >/dev/null
wait_for "servewaf logged no report of the last ordinary request" logged servewaf \
'"servewaf_line":"report"' '"path":"/api/v1/settings/api"'
refusals="$(refused)"
[ -z "$refusals" ] || fail "servewaf refused ordinary requests:
$refusals"
}
# attacks: send SQL injection, path traversal and script injection in a query
# string, and fail unless the Core Rule Set refuses each.
attacks() {
# The traversal climbs one directory: the default rule file refuses two or
# more first, before the Core Rule Set runs.
for query in "id=1'%20OR%20'1'%3D'1" "id=..%2Fcustom%2Fconf%2Fapp.ini" \
"id=%3Cscript%3Ealert(1)%3C%2Fscript%3E"; do
status="$(send "/owner/repo?$query")"
[ "$status" = 403 ] || fail "the attack ?$query was answered $status"
wait_for "servewaf logged no refusal of ?$query" logged servewaf \
'"servewaf_line":"report"' '"action":"waf_blocked"' "\"query\":\"$query\""
done
}
main() {
cd "$ROOT"
trap cleanup EXIT
@@ -204,6 +275,12 @@ main() {
esac
echo "gitea-setup: a request from the shared network skips the checks"
site_git -C "$WORK/clone" fetch --quiet || fail "the fetch failed"
ordinary_requests
echo "gitea-setup: the Core Rule Set refuses no ordinary gitea request"
attacks
echo "gitea-setup: the Core Rule Set refuses the attacks"
compose stop servewaf >/dev/null 2>&1 || fail "servewaf did not stop"
status="$(send /owner/repo)"
[ "$status" = 200 ] ||