SPEC: gitea path parameters, runners, uploads, GeoJS, tokens (closes #6)

Fourth review of the spec update:

- The Core Rule Set's lists of system files, shell paths and command
  names no longer check the query parameters and cookie in which gitea
  sends file paths, branch names and sign-in redirects; searches stay
  checked.
- The gitea notes name uploads cut off by the size and time limits,
  upload-artifact v3's refused Content-Range header, and runners that
  pass the day limit, with RATE_LIMIT_EXEMPT_NETS for them.
- GeoJS is asked about at most 200 addresses at once; an answer without
  country_code counts as unknown.
- A set token shorter than 32 characters, or a RULES_DIR that does not
  exist, stops the start.

Model: opus-5-5
This commit is contained in:
2026-09-28 20:18:49 +00:00
parent 4020ff83c6
commit 44a95a6396
+106 -51
View File
@@ -301,8 +301,9 @@ The settings, by group:
sidecar forwards and for its own endpoints (see "Admin endpoints"). sidecar forwards and for its own endpoints (see "Admin endpoints").
- `ADMIN_TOKEN`: bearer token for the ban endpoints and - `ADMIN_TOKEN`: bearer token for the ban endpoints and
`/_smallwebwaf/clients/<ip>`, a long random value, since they can be `/_smallwebwaf/clients/<ip>`, a long random value, since they can be
reached from the internet. Unset by default, which switches them off; bans reached from the internet. A token shorter than 32 characters stops the
are then managed by editing `bans.json`. start with a message naming the variable. Unset by default, which switches
them off; bans are then managed by editing `bans.json`.
- `INSTANCE_NAME` (default: the host name in `UPSTREAM_URL`, for example - `INSTANCE_NAME` (default: the host name in `UPSTREAM_URL`, for example
`gitea`): included in every log line, metric and alert. Set it, for `gitea`): included in every log line, metric and alert. Set it, for
example to `fsn1app1/gitea`, when several sidecars report to one place. example to `fsn1app1/gitea`, when several sidecars report to one place.
@@ -351,8 +352,9 @@ The settings, by group:
`INSTANCE_NAME`): syslog header fields. `INSTANCE_NAME`): syslog header fields.
- Metrics - Metrics
- `METRICS_TOKEN`: bearer token a scraper sends for `/_smallwebwaf/metrics`, - `METRICS_TOKEN`: bearer token a scraper sends for `/_smallwebwaf/metrics`,
a long random value. Unset by default, which switches the metrics off, a long random value. A token shorter than 32 characters stops the start
since they would otherwise be open to anyone on the internet. with a message naming the variable. Unset by default, which switches the
metrics off, since they would otherwise be open to anyone on the internet.
- `METRICS_TOP_N` (default `50`): how many AS numbers and countries get - `METRICS_TOP_N` (default `50`): how many AS numbers and countries get
their own series; the rest are summed as `other`. their own series; the rest are summed as `other`.
- Static lists - Static lists
@@ -363,7 +365,10 @@ The settings, by group:
- Request rate limits, per client (R1, R2). Breaking one bans the client (see - Request rate limits, per client (R1, R2). Breaking one bans the client (see
"Bans"), so the defaults sit several times above what one busy person "Bans"), so the defaults sit several times above what one busy person
produces: a browser loading a heavy page makes a few hundred requests, a git produces: a browser loading a heavy page makes a few hundred requests, a git
clone a handful, and several people often share one address. clone a handful, and several people often share one address. A machine that
talks to the app all day, such as a gitea Actions runner (see the notes
specific to gitea under "Deployment as a sidecar"), can still pass the day
limit; its address belongs in `RATE_LIMIT_EXEMPT_NETS`.
- `RATE_LIMIT_PER_MINUTE` (default `1000`), `RATE_LIMIT_PER_HOUR` (default - `RATE_LIMIT_PER_MINUTE` (default `1000`), `RATE_LIMIT_PER_HOUR` (default
`10000`), `RATE_LIMIT_PER_DAY` (default `50000`). `10000`), `RATE_LIMIT_PER_DAY` (default `50000`).
- `RATE_LIMIT_EXEMPT_PATHS`: path prefixes not counted (static assets, - `RATE_LIMIT_EXEMPT_PATHS`: path prefixes not counted (static assets,
@@ -430,15 +435,15 @@ The settings, by group:
the file rather than the file itself: docker does not show a single the file rather than the file itself: docker does not show a single
mounted file being replaced on the host. mounted file being replaced on the host.
- `geojs` asks the free GeoJS web service, which needs no account or key, - `geojs` asks the free GeoJS web service, which needs no account or key,
about several addresses in one request about up to 200 addresses in one request
(`https://get.geojs.io/v1/ip/geo.json?ip=a,b,c`), and reads each answer's (`https://get.geojs.io/v1/ip/geo.json?ip=a,b,c`), and reads each answer's
`country_code`, `asn` and `organization_name` (the AS name). A `country_code`, `asn` and `organization_name` (the AS name). An answer
`country_code` of `null`, which GeoJS gives for some ranges, and an `asn` without a `country_code`, which GeoJS sends for ranges it cannot place,
of `64512`, which it gives when it knows none, count as unknown. GeoJS is and an `asn` of `64512`, which it gives when it knows none, count as
told the address of every new visitor, whether or not a setting uses the unknown. GeoJS is told the address of every new visitor, whether or not a
answer, since the request log, the metrics, the client's history and ban setting uses the answer, since the request log, the metrics, the client's
notes carry the AS number and country. Private, loopback and link-local history and ban notes carry the AS number and country. Private, loopback
addresses, which no source can place, are never sent. and link-local addresses, which no source can place, are never sent.
- Each GeoJS answer is kept for 7 days in memory and in `lookups.json`, - Each GeoJS answer is kept for 7 days in memory and in `lookups.json`,
apart from the table of clients, so it survives a restart and outlasts a apart from the table of clients, so it survives a restart and outlasts a
client dropped from that table; after 7 days the client's next request client dropped from that table; after 7 days the client's next request
@@ -448,10 +453,11 @@ The settings, by group:
first answer when a setting needs it (see "Data flow for one request"); first answer when a setting needs it (see "Data flow for one request");
GeoJS normally answers in a fraction of that. At most one request to GeoJS GeoJS normally answers in a fraction of that. At most one request to GeoJS
is under way at a time, the addresses that arrive meanwhile are asked is under way at a time, the addresses that arrive meanwhile are asked
about together in the next one, and a request that takes longer than about together in the next one, up to 200 per request with the rest in the
`LOOKUP_TIMEOUT` is abandoned. A client whose answer has not come in time requests after it, and a request that takes longer than `LOOKUP_TIMEOUT`
counts as unknown until it comes: its later requests do not wait, and its is abandoned. A client whose answer has not come in time counts as unknown
address is asked about again in the background. until it comes: its later requests do not wait, and its address is asked
about again in the background.
- GeoJS publishes no rate limit, but its terms forbid "an excessive amount - GeoJS publishes no rate limit, but its terms forbid "an excessive amount
of API requests", judged by GeoJS alone, and let it block a caller. While of API requests", judged by GeoJS alone, and let it block a caller. While
GeoJS is slow, down or refusing the sidecar, clients with a kept answer GeoJS is slow, down or refusing the sidecar, clients with a kept answer
@@ -532,7 +538,7 @@ The settings, by group:
- `WAF_PARANOIA_LEVEL` (default `1`), `WAF_ANOMALY_THRESHOLD` (default `5`): - `WAF_PARANOIA_LEVEL` (default `1`), `WAF_ANOMALY_THRESHOLD` (default `5`):
the Core Rule Set's own two tuning values, at the Core Rule Set's own the Core Rule Set's own two tuning values, at the Core Rule Set's own
defaults. defaults.
- The sidecar also changes the Core Rule Set 4.25.0 in four ways that no - The sidecar also changes the Core Rule Set 4.25.0 in five ways that no
setting undoes, since in front of gitea each would otherwise refuse setting undoes, since in front of gitea each would otherwise refuse
ordinary requests: ordinary requests:
- PUT, PATCH and DELETE are allowed methods besides GET, HEAD, POST and - PUT, PATCH and DELETE are allowed methods besides GET, HEAD, POST and
@@ -550,6 +556,21 @@ The settings, by group:
git-credential-oauth and tea, which gitea registers for OAuth sign-in git-credential-oauth and tea, which gitea registers for OAuth sign-in
out of the box, ask to be sent back to `http://127.0.0.1` on the out of the box, ask to be sent back to `http://127.0.0.1` on the
user's own machine, and the server never fetches that address. user's own machine, and the server never fetches that address.
- The query parameters in which gitea sends file paths, branch and
workflow names and the page to return to after signing in (`path`,
`files`, `skip-to`, `sub_path`, `ref`, `sha`, `branch`, `workflow`,
`artifactName` and `redirect_to`), and the `redirect_to` cookie, are
not checked against the Core Rule Set's lists of system files
(930120), shell paths (932160) and command names (932260). In a
repository any name on those lists can be an ordinary file or branch,
such as `.gitignore`, `package.json`, `docker-compose.yml`,
`bin/docker-entrypoint` or a branch named `docker-build`, and gitea
reads these values as names within a repository or its own records, or
as a page of its own site. What only these three rules refuse, such as
`/etc/passwd` or `whoami` on its own, is therefore let through in
those parameters; path traversal (`../`), SQL and script injection and
PHP, Java and Node.js code are still refused there, and every other
parameter and cookie keeps all three rules.
- Responses are not inspected. A raw file from a repository, such as a - Responses are not inspected. A raw file from a repository, such as a
shell script, looks to the response rules like source code leaking shell script, looks to the response rules like source code leaking
from the server. from the server.
@@ -749,8 +770,10 @@ removed there takes effect while the sidecar runs.
stops the process with a message naming the file and line. While running, the stops the process with a message naming the file and line. While running, the
same faults leave the rules as they were, including the earlier version of same faults leave the rules as they were, including the earlier version of
that file, and the log and one `file_error` alert name the file and line; once that file, and the log and one `file_error` alert name the file and line; once
the file is fixed, it is read again. A missing or empty directory is not an the file is fixed, it is read again. A `RULES_DIR` that does not exist stops
error: the log says that no rules were loaded. the start with a message naming it. An empty directory is not an error: the
log says that no rules were loaded. To run without rule files, mount an empty
directory or set `RULES_ENABLED=false`.
- The image ships a default file in `RULES_DIR`. Mounting a directory over it - The image ships a default file in `RULES_DIR`. Mounting a directory over it
replaces the defaults; mounting single files into it adds to them. Docker does replaces the defaults; mounting single files into it adds to them. Docker does
not show a single mounted file being replaced on the host, which is how many not show a single mounted file being replaced on the host, which is how many
@@ -1012,9 +1035,10 @@ scrapers reach these endpoints through traefik, like any other request.
history, lookup result, reputation, offences, and bans with their notes; for history, lookup result, reputation, offences, and bans with their notes; for
answering "why was this address refused" and "what has this netblock been answering "why was this address refused" and "what has this netblock been
doing". doing".
- A token is sent as `Authorization: Bearer <token>`. While a token is unset, - A token is sent as `Authorization: Bearer <token>`, and one set shorter than
the endpoints that need it answer `404`, as does any other path under the 32 characters stops the start (see "Configuration surface"). While a token is
prefix. unset, the endpoints that need it answer `404`, as does any other path under
the prefix.
- Apart from the health check, these requests go through every check that any - Apart from the health check, these requests go through every check that any
other request goes through, and are answered at the point where another other request goes through, and are answered at the point where another
request would be forwarded to the app: a banned or refused client stays request would be forwarded to the app: a banned or refused client stays
@@ -1100,30 +1124,59 @@ networks:
a remote log endpoint, and an app-specific rule file. a remote log endpoint, and an app-specific rule file.
- Notes specific to gitea: - Notes specific to gitea:
- A clone is a response, and fits the defaults of 30 minutes and 5 GiB for - A clone is a response, and fits the defaults of 30 minutes and 5 GiB for
all but the largest repositories and slowest links. A push is a request: all but the largest repositories and slowest links. A push is a request,
at the defaults, one larger than 100 MiB or taking more than 60 seconds to and so is every other upload: at the defaults, one larger than 100 MiB or
upload is cut off, so a gitea that takes large pushes needs taking more than 60 seconds is cut off, whether it is a git push, an LFS
`CLIENT_REQUEST_MAX_BYTES`, `UPSTREAM_REQUEST_MAX_BYTES`, object, a container image layer, a package file or a release attachment.
`CLIENT_REQUEST_TIMEOUT` and `UPSTREAM_REQUEST_TIMEOUT` raised to fit. The The client is answered `413` for a body that is too large, before anything
Core Rule Set does not read a pack upload, which streams through without reaches gitea when the request announces its size, or `408` for one that
being held in memory. is too slow; the upload fails, and no one is banned for it. A gitea that
takes large uploads needs `CLIENT_REQUEST_MAX_BYTES`,
`UPSTREAM_REQUEST_MAX_BYTES`, `CLIENT_REQUEST_TIMEOUT` and
`UPSTREAM_REQUEST_TIMEOUT` raised to fit. The Core Rule Set does not read
an upload's body, which streams through without being held in memory.
- At the defaults (see "Configuration surface", attack detection), the Core - At the defaults (see "Configuration surface", attack detection), the Core
Rule Set lets gitea's ordinary use through: browsing and views of files in Rule Set lets gitea's ordinary use through, whatever its files and
a repository, git's clone, fetch and push over HTTP, signing in, including branches are called: browsing and views of files in a repository, with
with Git Credential Manager, git-credential-oauth or tea, the API, pushing their history, blame and the file tree; diffs, including their hidden
and pulling container images and packages, and posting issues, pull lines and large files, and pull request review; git's clone, fetch and
requests, comments, wiki pages and files saved in the web editor, code push over HTTP; signing in, including the return to the page a visitor
included, since no body is read. It can still refuse a request whose URL came from and sign-in with Git Credential Manager, git-credential-oauth or
reads to it as an attack: a search, or another query string, holding a tea; the API's calls for a file and its commits; pushing and pulling
shell command with its options or a system path (`ls -la`, `sed -i`, container images and packages; Actions runners, and artifacts uploaded
`/bin/sh`), a command in backticks, script code (`fetch(`, `${VAR}`), HTML with `actions/upload-artifact@v4`; and posting issues, pull requests,
(`<img src=`), an SQL statement (`SELECT * FROM users WHERE`), or a URL comments, wiki pages and files saved in the web editor, code included,
naming an IP address or `localhost`; or a path such as a file name ending since no body is read. It can still refuse a query string that reads to it
in `~`, or an `.xhtml` file whose path holds a space. Such a refusal as an attack, most often a search: one for a name on its lists of system
answers only that request, with 403, and bans no one by itself: it counts files and commands, such as `package.json`, `.gitignore` or
toward the error burst, which a person searching does not reach. The `docker-compose.yml`, or for text that starts with a command name, such as
request log names the rule in `waf_rule_ids`, which `WAF_DISABLED_RULES` `python3` or `ssh key`; or one holding a shell command with its options or
can switch off. a system path (`ls -la`, `sed -i`, `/bin/sh`), a command in backticks,
script code (`fetch(`, `${VAR}`, `process.env`), HTML (`<img src=`), an
SQL statement (`SELECT * FROM users WHERE`), or a URL naming an IP address
or `localhost`. The lists refuse such a name in any other query parameter
too, such as a release attachment uploaded through the API as
`docker-compose.yml`. A path can be refused as well: a file name ending in
`~`, or an `.xhtml` file whose path holds a space. Such a refusal answers
only that request, with 403, and bans no one by itself: it counts toward
the error burst, which a person searching does not reach. The request log
names the rule in `waf_rule_ids`, which `WAF_DISABLED_RULES` can switch
off.
- Artifact uploads from `actions/upload-artifact@v3` send the header
`Content-Range`, which the Core Rule Set refuses (920450). Each file of
the artifact is refused with 403 and the step fails; an artifact of more
than 30 files also bans the runner through the error burst.
`actions/upload-artifact@v4` does not send the header.
- An Actions runner that reaches gitea through the sidecar 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 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 `RATE_LIMIT_EXEMPT_NETS`, which takes them out of
the request and byte limits while the error burst, attack detection and
bans still apply.
- Archive download and blame or history pages are what scrapers hammer; - Archive download and blame or history pages are what scrapers hammer;
request limits do most of the work there. request limits do most of the work there.
@@ -1190,14 +1243,16 @@ networks:
no one by itself; the request log names the rule, and exclusions by rule id no one by itself; the request log names the rule, and exclusions by rule id
and path fix it. and path fix it.
- Attacks carried in request bodies: not refused by default, since on a code - Attacks carried in request bodies: not refused by default, since on a code
forge bodies are full of code the Core Rule Set takes for attacks. What shows forge bodies are full of code the Core Rule Set takes for attacks. Most of
in URLs and headers is still refused, the client that sends an attack is still what shows in URLs and headers is still refused, the client that sends an
subject to the rule files, the limits and the bans, and `WAF_BODY_LIMIT` attack is still subject to the rule files, the limits and the bans, and
switches body inspection on for apps whose forms carry no code. `WAF_BODY_LIMIT` switches body inspection on for apps whose forms carry no
code.
- The admin endpoints can be reached from the internet: all but the health check - The admin endpoints can be reached from the internet: all but the health check
need a token and are off while it is unset, and a missing or wrong token need a token and are off while it is unset, and a missing or wrong token
counts toward the error burst, so a client guessing tokens is soon banned. counts toward the error burst, so a client guessing tokens is soon banned.
Tokens are meant to be long random values. Tokens are meant to be long random values, and one shorter than 32 characters
stops the start.
- GeoJS, the default lookup source: every new visitor's address goes to a third - GeoJS, the default lookup source: every new visitor's address goes to a third
party, and a swarm of fresh addresses, when lookups peak, is when GeoJS may party, and a swarm of fresh addresses, when lookups peak, is when GeoJS may
slow down or block the sidecar. Keeping answers for 7 days and asking about slow down or block the sidecar. Keeping answers for 7 days and asking about