check / check (push) Waiting to run
SPEC.md is the only place that says how smallwebwaf behaves and why; docs/ holds lists linking to it; the README is short and links to both. Passed independent review in round 8. Model: fable-5-1 (writing); opus-5-5 (review, merge)
216 lines
10 KiB
Markdown
216 lines
10 KiB
Markdown
# smallwebwaf in front of gitea
|
|
|
|
These notes record what `smallwebwaf` does in front of gitea at the default
|
|
settings. They are observations, not requirements; the requirements are in
|
|
[`SPEC.md`](../SPEC.md). `script/gitea-requests` runs gitea behind the image and
|
|
sends through it a sample of gitea's ordinary use and the requests named below.
|
|
It reports each request the Core Rule Set refuses, so that these notes can be
|
|
checked again when gitea or the Core Rule Set changes version.
|
|
|
|
## What the defaults cover
|
|
|
|
- SSH access to gitea does not pass through `smallwebwaf` and is not protected
|
|
by it.
|
|
- A clone is a response. It fits the defaults of 30 minutes and 5 GiB for all
|
|
but the largest repositories and slowest links.
|
|
- A push is a request, and so is every other upload:
|
|
- a git push;
|
|
- an LFS object;
|
|
- a container image layer;
|
|
- a package file;
|
|
- a release attachment.
|
|
|
|
At the defaults, an upload larger than 100 MiB or taking more than 60
|
|
seconds is cut off ("Size and time limits" in `SPEC.md`). A gitea that takes
|
|
large uploads needs `SWWAF_REQUEST_MAX_BYTES`,
|
|
`SWWAF_CLIENT_REQUEST_TIMEOUT` and `SWWAF_UPSTREAM_REQUEST_TIMEOUT` raised
|
|
to fit.
|
|
|
|
- Archive download and blame or history pages are what scrapers hammer. The
|
|
request limits do most of the work there.
|
|
- An Actions runner that reaches gitea through `smallwebwaf` sends requests all
|
|
day. One older than version 0.4 (April 2026) asks for work every 2 seconds,
|
|
43,200 requests a day, and reports a running job's log and state every second.
|
|
So it passes the day limit of 50,000 after about an hour and a quarter of
|
|
jobs. It is then banned, the job it is running fails, and each repeat within a
|
|
day makes the ban longer. Newer runners ask less often, but a busy one, or
|
|
several on one address, can still pass it. Put the runners' addresses in
|
|
`SWWAF_RATE_LIMIT_EXEMPT_NETS`.
|
|
|
|
## What the Core Rule Set lets through
|
|
|
|
At the defaults, the Core Rule Set lets gitea's ordinary use through, apart from
|
|
the refusals in the next section:
|
|
|
|
- browsing and views of files in a repository, with their history, blame and the
|
|
file tree;
|
|
- diffs, including their hidden lines and large files, and pull request review;
|
|
- git's clone, fetch and push over HTTP;
|
|
- signing in, including the return to the page a visitor came from, and sign-in
|
|
with Git Credential Manager, git-credential-oauth or tea;
|
|
- the API's calls for a file and its commits;
|
|
- pushing and pulling container images and packages;
|
|
- Actions runners, and artifacts uploaded with `actions/upload-artifact@v4`;
|
|
- posting issues, pull requests, comments, wiki pages and files saved in the web
|
|
editor, code included, since no body is read;
|
|
- the page gitea shows after a change, with its message naming what was done.
|
|
|
|
## What the Core Rule Set still refuses
|
|
|
|
Each refusal answers only that request ("Bans" in `SPEC.md` says what it counts
|
|
toward). The request log names the rule in `waf_rule_ids`, which
|
|
`SWWAF_WAF_DISABLED_RULES` can switch off.
|
|
|
|
### A query string that reads as an attack
|
|
|
|
Most often this is a search. A search term is a query parameter, which the Core
|
|
Rule Set checks against its lists of system files and commands (see the fourth
|
|
change under "Attack detection" in `SPEC.md`). A release attachment uploaded
|
|
through the API as `docker-compose.yml` is refused the same way. Examples:
|
|
|
|
- a name on those lists, such as `package.json`, `.gitignore` or
|
|
`docker-compose.yml`;
|
|
- text that starts with a command name, such as `python3` or `ssh key`, or has
|
|
one right after a `/` in its first word, such as `feat/docker-support`;
|
|
- a shell command with its options or a system path (`ls -la`, `sed -i`,
|
|
`/bin/sh`), or a command in backticks;
|
|
- script code (`fetch(`, `${VAR}`, `process.env`), HTML (`<img src=`) or an SQL
|
|
statement (`SELECT * FROM users WHERE`);
|
|
- a URL naming an IP address or `localhost`.
|
|
|
|
### A branch, tag or file name that starts with a command name
|
|
|
|
The Core Rule Set's list of commands (932260) holds names such as `docker-`,
|
|
`python3`, `ansible`, `base64` and `whoami`. A query parameter holding a branch,
|
|
tag or file name that starts with one is refused. So is one that has such a name
|
|
right after a `/`, such as `feat/docker-support`, `fix/docker-build` or
|
|
`renovate/docker-build-push-action-6.x`. gitea's own pages send the name in a
|
|
query parameter that 930120, 932160 and 932260 still check:
|
|
|
|
- `refSubUrl`, the branch or tag of a directory listing. A listing sends it when
|
|
it asks for its entries' last commits in a second request. It does so whenever
|
|
gitea takes more than a second to work them out;
|
|
- `name`, when a branch is deleted or restored on the branches page;
|
|
- `tag`, when a release is started from a tag;
|
|
- `template`, the file of an issue template, when an issue is opened from it;
|
|
- `rule_name`, when a branch protection rule is opened for editing in the
|
|
repository's settings or has just been saved.
|
|
|
|
What a visitor sees, for a branch named `docker-build`:
|
|
|
|
- A directory listing that makes that second request leaves those last commits
|
|
out and shows an error.
|
|
- The branches page shows an error instead of deleting or restoring the branch.
|
|
- A release started from such a tag gets the `403` answer of `smallwebwaf` in
|
|
place of its form. So does an issue opened from such a template, and such a
|
|
rule opened for editing.
|
|
- Saving such a rule stores it. gitea then returns to the branch settings page,
|
|
which gets the `403` answer, so the save looks as if it failed.
|
|
|
|
### A file name in a query parameter that trips the code rules
|
|
|
|
The Core Rule Set's rules for Java, script, PowerShell and SQL find what they
|
|
look for in some file names. This holds too in the ten query parameters that
|
|
930120, 932160 and 932260 do not check (see the fourth change under "Attack
|
|
detection" in `SPEC.md`). Examples:
|
|
|
|
- `runtime` in OpenJDK's `src/java.base/share/classes/java/lang/Runtime.java`
|
|
(944110);
|
|
- a Java class, as in `java.lang.String.md` (944130);
|
|
- `document.write.js` (941180);
|
|
- `process.env.d.ts` (934100);
|
|
- `__proto__` (934130);
|
|
- the PowerShell command `Install-Script`, as in `install-script.sh` (932120);
|
|
- a database's name, as in PostgreSQL's `information_schema.sql` (942140).
|
|
|
|
For such a file, these requests are refused:
|
|
|
|
- the API's list of its commits (`path`);
|
|
- the API's call for the contents of files that names it in its query (`body`);
|
|
- the commit graph of that file alone (`file`);
|
|
- the hidden lines of its diff (`path`), which stay hidden;
|
|
- the sign-in page, for a visitor who opens a page of the file that needs
|
|
signing in, such as its edit page (`redirect_to`). The visitor can still sign
|
|
in by opening the sign-in page itself.
|
|
|
|
### A path
|
|
|
|
- A file name ending in `~`, or an `.xhtml` file whose path holds a space.
|
|
- A path that holds what the Core Rule Set takes for a Java class (944130), as
|
|
`java.lang.String.md` and
|
|
`META-INF/services/org.apache.commons.logging.LogFactory` do.
|
|
- A path that holds what it takes for Node.js code (934100), as
|
|
`process.env.d.ts` does.
|
|
- A path with a directory named `...` (930100, 930110), such as `.../notes.txt`.
|
|
|
|
For such a file, these are refused:
|
|
|
|
- the page of the file;
|
|
- its raw file;
|
|
- its history and its blame;
|
|
- its edit page.
|
|
|
|
The directory that holds it lists it, and a clone carries it.
|
|
|
|
### A container image push that reuses a layer
|
|
|
|
A push can reuse a layer of an image docker has pushed to that gitea before.
|
|
docker then asks gitea to take the layer from that image, naming it in the query
|
|
parameter `from`, which 932260 checks. The request is refused when the image's
|
|
owner starts with an entry on the list of commands. It is refused too when the
|
|
image's name after the owner starts with one. Examples are `owner/python3-app`,
|
|
`owner/docker-base`, and any image of an owner named `ansible` or `docker-team`.
|
|
|
|
What happens next depends on docker's image store. A docker that keeps its
|
|
images in containerd's image store fails the push, and fails each new try the
|
|
same way. A new install of Docker Engine 29 does this. A docker that keeps them
|
|
in its own older image store uploads the layer instead. The push then succeeds
|
|
after one refusal for each layer it reuses, and each of those refusals counts
|
|
toward the error burst ("Attack detection" in `SPEC.md`).
|
|
|
|
### Artifact uploads from `actions/upload-artifact@v3`
|
|
|
|
This action sends the header `Content-Range`, which the Core Rule Set refuses
|
|
(920450). The action sends two files at a time, does not retry a `403`, and
|
|
sends no further file once one is refused. So an upload is refused once or
|
|
twice, however many files it holds, and the step fails. One upload does not ban
|
|
the runner. The error burst ("Attack detection" in `SPEC.md`) bans it after more
|
|
than 30 such refusals from its address in a minute, as when many jobs upload at
|
|
once. `actions/upload-artifact@v4` does not send the header.
|
|
|
|
## What body inspection refuses
|
|
|
|
With `SWWAF_WAF_BODY_LIMIT` set, the Core Rule Set reads request bodies too. In
|
|
front of gitea it then refuses these, until their rule ids, named in the request
|
|
log, are added to `SWWAF_WAF_DISABLED_RULES`:
|
|
|
|
- issue and comment text, wiki pages and files saved in the web editor that hold
|
|
shell commands or code (932125, 932235, 932250 and others);
|
|
- package descriptions that show code;
|
|
- PyPI uploads (922130);
|
|
- attachments named like `debug.log` or `config.yml` (932180).
|
|
|
|
## What `script/gitea-requests` sends
|
|
|
|
Of the requests above, the script sends:
|
|
|
|
- for each file named under "A file name in a query parameter that trips the
|
|
code rules", the API's list of its commits;
|
|
- for `Runtime.java` alone, the commit graph of that file, the hidden lines of
|
|
its diff, and the API's call for the contents of files that names it;
|
|
- the sign-in page for a visitor who opens the edit page of `document.write.js`;
|
|
- the pages of `java.lang.String.md`,
|
|
`META-INF/services/org.apache.commons.logging.LogFactory`, `process.env.d.ts`
|
|
and `.../notes.txt`;
|
|
- one container image push that reuses a layer, which it does not try again.
|
|
|
|
It does not send the raw file, history, blame or edit page of those four files,
|
|
or the pages of the directories `META-INF/services` and `...`. It also sends a
|
|
short sample of gitea's ordinary use:
|
|
|
|
- signing in;
|
|
- pages of files and commits, and a diff;
|
|
- the API;
|
|
- a push, and clone and fetch over HTTP;
|
|
- two container image pushes.
|