Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
478c3affad |
No files matched your search
@@ -10,11 +10,10 @@ A web application firewall reads each request on its way to a web app and
|
|||||||
refuses the ones that look like abuse. `smallwebwaf` is one program, `sww`,
|
refuses the ones that look like abuse. `smallwebwaf` is one program, `sww`,
|
||||||
shipped in one image, with two commands that do this together:
|
shipped in one image, with two commands that do this together:
|
||||||
|
|
||||||
- `sww rproxy` is a small reverse proxy in front of one app. It sits behind the
|
- `sww rproxy` is a small reverse proxy in front of one app. traefik routes to
|
||||||
reverse proxy that terminates TLS, such as traefik, nginx or Caddy. It asks
|
it. It asks `waf` about each request, passes the allowed ones to the app, and
|
||||||
`waf` about each request, passes the allowed ones to the app, and passes the
|
passes the app's answers back. It keeps nothing between requests and writes
|
||||||
app's answers back. It keeps nothing between requests and writes nothing to
|
nothing to disk.
|
||||||
disk.
|
|
||||||
- `sww waf` decides. It runs the checks and keeps the bans, the counters and
|
- `sww waf` decides. It runs the checks and keeps the bans, the counters and
|
||||||
each client's history. One `waf` serves many sites: a client banned through
|
each client's history. One `waf` serves many sites: a client banned through
|
||||||
one site is refused on all of them, and its requests on all of them count
|
one site is refused on all of them, and its requests on all of them count
|
||||||
@@ -58,8 +57,8 @@ For the operator, it:
|
|||||||
|
|
||||||
Not built, and not planned:
|
Not built, and not planned:
|
||||||
|
|
||||||
- TLS and routing by host name. Both stay with the reverse proxy in front of
|
- TLS and routing by host name. Both stay with traefik, the reverse proxy in
|
||||||
`rproxy`.
|
front of `rproxy`.
|
||||||
- Browser challenges such as captchas.
|
- Browser challenges such as captchas.
|
||||||
- A web console, or a configuration file.
|
- A web console, or a configuration file.
|
||||||
- Defence against floods large enough to fill the host's network link.
|
- Defence against floods large enough to fill the host's network link.
|
||||||
@@ -91,48 +90,20 @@ accepts connections, whether or not `waf` answers; in `waf`'s container, once
|
|||||||
The usual setup is three containers on one private network:
|
The usual setup is three containers on one private network:
|
||||||
|
|
||||||
- the app;
|
- the app;
|
||||||
- `rproxy`, in front of the app, which the reverse proxy that terminates TLS
|
- `rproxy`, in front of the app, which traefik routes to:
|
||||||
forwards to: `SWW_RPROXY_WAF_BACKEND` names `waf` and
|
`SWW_RPROXY_WAF_BACKEND` names `waf` and `SWW_RPROXY_UPSTREAM_URL` names the
|
||||||
`SWW_RPROXY_UPSTREAM_URL` names the app;
|
app;
|
||||||
- `waf`, with `SWW_COMMAND=waf` and a volume at `/var/lib/sww` for its state
|
- `waf`, with `SWW_COMMAND=waf` and a volume at `/var/lib/sww` for its state
|
||||||
files.
|
files.
|
||||||
|
|
||||||
Publish no port of the app or of `waf`. Anything on that network can reach the
|
Point traefik's labels at port 8080 of `rproxy`
|
||||||
app around `rproxy`, and can ask `waf` questions, so the network holds only what
|
(`traefik.http.services.<name>.loadbalancer.server.port=8080`). Publish no port
|
||||||
you trust. Each further app adds its own container and its own `rproxy`, whose
|
of the app or of `waf`. Anything on that network can reach the app around
|
||||||
|
`rproxy`, and can ask `waf` questions, so the network holds only what you trust.
|
||||||
|
Each further app adds its own container and its own `rproxy`, whose
|
||||||
`SWW_RPROXY_WAF_BACKEND` names the same `waf`. The "Smallest setup" example
|
`SWW_RPROXY_WAF_BACKEND` names the same `waf`. The "Smallest setup" example
|
||||||
below shows this shape.
|
below shows this shape.
|
||||||
|
|
||||||
### The reverse proxy in front
|
|
||||||
|
|
||||||
`rproxy` serves plain HTTP. It runs behind any reverse proxy that terminates
|
|
||||||
TLS, such as traefik, nginx or Caddy. That proxy must:
|
|
||||||
|
|
||||||
- forward each request to port 8080 of `rproxy` over plain HTTP;
|
|
||||||
- keep the visitor's `Host` header;
|
|
||||||
- add the visitor's address to `X-Forwarded-For`;
|
|
||||||
- set `X-Forwarded-Proto` to the scheme the visitor used;
|
|
||||||
- connect to `rproxy` from an address in `SWW_RPROXY_TRUSTED_PROXIES`, by
|
|
||||||
default `10.0.0.0/8`, `172.16.0.0/12` or `192.168.0.0/16`.
|
|
||||||
|
|
||||||
From any other address, `rproxy` ignores the forwarded headers and takes the
|
|
||||||
proxy for the visitor. Every visitor then shares one address, and one ban.
|
|
||||||
|
|
||||||
traefik and Caddy keep the `Host` header and set both forwarded headers by
|
|
||||||
default. traefik's Docker label
|
|
||||||
`traefik.http.services.<name>.loadbalancer.server.port=8080` names the port, and
|
|
||||||
Caddy's `reverse_proxy rproxy:8080` needs nothing more. nginx sets none of the
|
|
||||||
headers by default, so its site names them:
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
location / {
|
|
||||||
proxy_pass http://rproxy:8080;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `rproxy` in the app's container
|
### `rproxy` in the app's container
|
||||||
|
|
||||||
An app's image can instead be built on the `smallwebwaf` image, so that `rproxy`
|
An app's image can instead be built on the `smallwebwaf` image, so that `rproxy`
|
||||||
@@ -183,13 +154,13 @@ entrypoint starts `rproxy` and the app side by side, each as its own user. Run
|
|||||||
the app's image with `SWW_RPROXY_WAF_BACKEND` set. `deploy/example-app` is such
|
the app's image with `SWW_RPROXY_WAF_BACKEND` set. `deploy/example-app` is such
|
||||||
an app, and the "`rproxy` beside the app" example below runs one.
|
an app, and the "`rproxy` beside the app" example below runs one.
|
||||||
|
|
||||||
The app sees the headers the reverse proxy in front sent, with that proxy's
|
The app sees the headers traefik sent, with traefik's address added to
|
||||||
address added to `X-Forwarded-For`, from a TCP peer that is `rproxy`. So the app
|
`X-Forwarded-For`, from a TCP peer that is `rproxy`. So the app must trust
|
||||||
must trust `rproxy`'s address for forwarded headers. In a container of its own,
|
`rproxy`'s address for forwarded headers. In a container of its own, `rproxy` is
|
||||||
`rproxy` is on the private network, which the usual default of a trusted-proxy
|
on the private network, which the usual default of a trusted-proxy setting
|
||||||
setting covers. Beside the app, its address is `127.0.0.1` or `::1`, which the
|
covers. Beside the app, its address is `127.0.0.1` or `::1`, which the private
|
||||||
private ranges leave out, so the app's own setting must name them too, as
|
ranges leave out, so the app's own setting must name them too, as `app.run`
|
||||||
`app.run` does.
|
does.
|
||||||
|
|
||||||
### Checking that it works
|
### Checking that it works
|
||||||
|
|
||||||
@@ -197,9 +168,9 @@ private ranges leave out, so the app's own setting must name them too, as
|
|||||||
`healthy` for each container of `smallwebwaf`.
|
`healthy` for each container of `smallwebwaf`.
|
||||||
- A log line with `"msg":"starting"` gives the version, the address and every
|
- A log line with `"msg":"starting"` gives the version, the address and every
|
||||||
setting in effect, with secrets masked.
|
setting in effect, with secrets masked.
|
||||||
- A request to the site through the reverse proxy in front gets the app's
|
- A request to the site through traefik gets the app's answer.
|
||||||
answer. `https://<site>/_sww/healthz` answers `ok`. `waf`'s log lines about
|
`https://<site>/_sww/healthz` answers `ok`. `waf`'s log lines about the
|
||||||
the request have `"action":"forward"`.
|
request have `"action":"forward"`.
|
||||||
- To see a refusal without banning yourself, run `waf` with
|
- To see a refusal without banning yourself, run `waf` with
|
||||||
`SWW_WAF_MODE=observe` and ask for `/.env`. The request reaches the app.
|
`SWW_WAF_MODE=observe` and ask for `/.env`. The request reaches the app.
|
||||||
`waf`'s log lines have `"would_action":"temporarily_banned"` and
|
`waf`'s log lines have `"would_action":"temporarily_banned"` and
|
||||||
@@ -289,7 +260,7 @@ Every setting the code reads, with its default:
|
|||||||
| `SWW_WAF_LOOKUP_SOURCE` | `geojs` | Where each client's AS number and country come from: `geojs` (a web service), `file` (a local database) or `off`. |
|
| `SWW_WAF_LOOKUP_SOURCE` | `geojs` | Where each client's AS number and country come from: `geojs` (a web service), `file` (a local database) or `off`. |
|
||||||
| `SWW_WAF_LOOKUP_DB_PATH` | empty | The IPinfo Lite database file, in its `.mmdb` form, read while the source is `file`. |
|
| `SWW_WAF_LOOKUP_DB_PATH` | empty | The IPinfo Lite database file, in its `.mmdb` form, read while the source is `file`. |
|
||||||
| `SWW_WAF_ASN_AND_COUNTRY_LOOKUP_TIMEOUT_MS` | `800` | How long a request waits for its client's first GeoJS answer when a setting needs it, and how long a request to GeoJS may take, in milliseconds. |
|
| `SWW_WAF_ASN_AND_COUNTRY_LOOKUP_TIMEOUT_MS` | `800` | How long a request waits for its client's first GeoJS answer when a setting needs it, and how long a request to GeoJS may take, in milliseconds. |
|
||||||
| `SWW_WAF_ADD_LOOKUP_HEADERS` | `true`, or `false` while the lookup source is `off` | `true` passes the app the client's AS number and country in `X-Client-ASN` and `X-Client-Country`. |
|
| `SWW_WAF_ADD_LOOKUP_HEADERS` | `false` | `true` passes the app the client's AS number and country in `X-Client-ASN` and `X-Client-Country`. |
|
||||||
| `SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES` | empty | When set, the only countries whose clients get through; a client the lookup cannot place is refused too. |
|
| `SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES` | empty | When set, the only countries whose clients get through; a client the lookup cannot place is refused too. |
|
||||||
| `SWW_WAF_TAG_LISTS` | empty | Address lists that each add a tag to their clients' requests, as `name=URL` pairs, such as `torexit=https://example.com/tor.txt`. |
|
| `SWW_WAF_TAG_LISTS` | empty | Address lists that each add a tag to their clients' requests, as `name=URL` pairs, such as `torexit=https://example.com/tor.txt`. |
|
||||||
| `SWW_WAF_TAG_DENY` | empty | Tags whose requests are refused, such as `torexit,country=CN`. |
|
| `SWW_WAF_TAG_DENY` | empty | Tags whose requests are refused, such as `torexit,country=CN`. |
|
||||||
@@ -569,10 +540,9 @@ request reaches the app.
|
|||||||
A refused visitor is answered with `SWW_WAF_BAN_RESPONSE`. A `block` rule or a
|
A refused visitor is answered with `SWW_WAF_BAN_RESPONSE`. A `block` rule or a
|
||||||
Core Rule Set match answers `403` and bans nobody, since the Core Rule Set has
|
Core Rule Set match answers `403` and bans nobody, since the Core Rule Set has
|
||||||
false positives; such refusals count toward `SWW_WAF_REFUSALS_LIMIT_PER_MINUTE`
|
false positives; such refusals count toward `SWW_WAF_REFUSALS_LIMIT_PER_MINUTE`
|
||||||
instead. A request over the header limits of step 3 is answered `414` for a
|
instead. A request over the header limits of step 3 is answered `431`, and bans
|
||||||
request line over 8 KiB and `431` otherwise. It bans nobody either. `rproxy`
|
nobody either. `rproxy` itself answers `405` to a method it does not pass on,
|
||||||
itself answers `405` to a method it does not pass on, and the status for a size
|
and the status for a size or time limit, such as `413` for a body too large.
|
||||||
or time limit, such as `413` for a body too large.
|
|
||||||
|
|
||||||
### Clients and bans
|
### Clients and bans
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
`smallwebwaf` is a web application firewall for self-hosted apps. It is one
|
`smallwebwaf` is a web application firewall for self-hosted apps. It is one
|
||||||
binary, `sww`, and two of its commands do the work:
|
binary, `sww`, and two of its commands do the work:
|
||||||
|
|
||||||
- `sww rproxy` is a reverse proxy between one app and the reverse proxy that
|
- `sww rproxy` is a reverse proxy between traefik and one app.
|
||||||
terminates TLS.
|
|
||||||
- `sww waf` decides what happens to each request, for many sites.
|
- `sww waf` decides what happens to each request, for many sites.
|
||||||
|
|
||||||
This document says what it must do and why, each requirement once. Everything in
|
This document says what it must do and why, each requirement once. Everything in
|
||||||
@@ -46,8 +45,7 @@ never as a time.
|
|||||||
|
|
||||||
## Non-goals
|
## Non-goals
|
||||||
|
|
||||||
- TLS, certificates and routing by host name. Those are the job of the reverse
|
- TLS, certificates and routing by host name. Those are traefik's job.
|
||||||
proxy in front of `rproxy`, such as traefik, nginx or Caddy.
|
|
||||||
- More than one app behind one `rproxy`. Each app has its own.
|
- More than one app behind one `rproxy`. Each app has its own.
|
||||||
- Browser challenges such as captchas or proof of work. If wanted, chain Anubis
|
- Browser challenges such as captchas or proof of work. If wanted, chain Anubis
|
||||||
between `rproxy` and the app.
|
between `rproxy` and the app.
|
||||||
@@ -153,26 +151,26 @@ field gives that name. `refusal_limit` and `byte_limits` give theirs in
|
|||||||
around its address, a /64 by default, since one abuser usually controls a
|
around its address, a /64 by default, since one abuser usually controls a
|
||||||
whole /64. A client is therefore one IPv4 address or one IPv6 group.
|
whole /64. A client is therefore one IPv4 address or one IPv6 group.
|
||||||
3. The header limits (`header_limits`; see "Answering in time"). They come first
|
3. The header limits (`header_limits`; see "Answering in time"). They come first
|
||||||
after `SWW_WAF_ALLOW_NETS`, since they only measure lines and count header
|
after `SWW_WAF_ALLOW_NETS`, since they only count header fields and cookies.
|
||||||
fields and cookies. Next, the tag lists (`tag_lists`; see "Request tags").
|
Next, the tag lists (`tag_lists`; see "Request tags"). Each list that holds
|
||||||
Each list that holds the client adds its tag. Next, `routewatch_tags` adds
|
the client adds its tag. Next, `routewatch_tags` adds the client's `asn=` and
|
||||||
the client's `asn=` and `prefix=` tags while `SWW_WAF_ROUTEWATCH_URL` is set.
|
`prefix=` tags while `SWW_WAF_ROUTEWATCH_URL` is set. A client in
|
||||||
A client in `SWW_WAF_ALLOW_NETS` skips the header limits and gets no tag.
|
`SWW_WAF_ALLOW_NETS` skips the header limits and gets no tag.
|
||||||
4. The static lists. A client in `SWW_WAF_ALLOW_NETS` skips steps 5 to 15, the
|
4. The static lists. A client in `SWW_WAF_ALLOW_NETS` skips steps 5 to 15, the
|
||||||
byte limits and the refusal limit. A client in `SWW_WAF_DENY_NETS` is refused
|
byte limits and the refusal limit. A client in `SWW_WAF_DENY_NETS` is refused
|
||||||
(`deny_nets`); a client in both lists is let through.
|
(`deny_nets`); a client in both lists is let through.
|
||||||
5. Bans. A client under a ban is refused (`active_ban`; see "Bans").
|
5. Bans. A client under a ban is refused (`active_ban`; see "Bans").
|
||||||
6. The lookup, unless `SWW_WAF_LOOKUP_SOURCE` is `off` (see "Lookup of AS number
|
6. The lookup, unless `SWW_WAF_LOOKUP_SOURCE` is `off` (see "Lookup of AS number
|
||||||
and country"). The request waits for the first GeoJS answer, up to
|
and country"). The request goes on at once. It waits for the first GeoJS
|
||||||
`SWW_WAF_ASN_AND_COUNTRY_LOOKUP_TIMEOUT_MS`, when one of these needs it:
|
answer, up to `SWW_WAF_ASN_AND_COUNTRY_LOOKUP_TIMEOUT_MS`, only when one of
|
||||||
- `SWW_WAF_ADD_LOOKUP_HEADERS`, which is `true` by default;
|
these needs it:
|
||||||
- the country list;
|
- the country list;
|
||||||
- a biased threshold;
|
- a biased threshold;
|
||||||
- `SWW_WAF_TAG_DENY` or `SWW_WAF_TAG_LIMIT_PERCENT` naming an `asn=` or
|
- `SWW_WAF_TAG_DENY` or `SWW_WAF_TAG_LIMIT_PERCENT` naming an `asn=` or
|
||||||
`country=` tag.
|
`country=` tag;
|
||||||
|
- `SWW_WAF_ADD_LOOKUP_HEADERS`.
|
||||||
|
|
||||||
Otherwise the request goes on at once. A request that ends before the answer
|
A request that ends before the answer comes is logged without it.
|
||||||
comes is logged without it.
|
|
||||||
|
|
||||||
7. The country list (`countries`; see "Country list").
|
7. The country list (`countries`; see "Country list").
|
||||||
8. The reputation sources (see "Reputation"), in this order:
|
8. The reputation sources (see "Reputation"), in this order:
|
||||||
@@ -199,10 +197,10 @@ field gives that name. `refusal_limit` and `byte_limits` give theirs in
|
|||||||
16. A path under `/_sww/` is answered `404`, so that the app never sees that
|
16. A path under `/_sww/` is answered `404`, so that the app never sees that
|
||||||
prefix.
|
prefix.
|
||||||
17. `rproxy` passes the request to `SWW_RPROXY_UPSTREAM_URL`, streaming (see
|
17. `rproxy` passes the request to `SWW_RPROXY_UPSTREAM_URL`, streaming (see
|
||||||
"Passing a request to the app"). While `SWW_WAF_ADD_LOOKUP_HEADERS` is
|
"Passing a request to the app"). With `SWW_WAF_ADD_LOOKUP_HEADERS`,
|
||||||
`true`, as by default, `X-Client-ASN` and `X-Client-Country` are added for
|
`X-Client-ASN` and `X-Client-Country` are added for the app's own logs; a
|
||||||
the app's own logs; a header whose value is unknown is left out. Whatever
|
header whose value is unknown is left out. Whatever that setting says, any
|
||||||
that setting says, any such header the client sent is removed.
|
such header the client sent is removed.
|
||||||
18. After the visitor's answer has ended, `rproxy` sends the report (see "The
|
18. After the visitor's answer has ended, `rproxy` sends the report (see "The
|
||||||
report"). `waf` then counts:
|
report"). `waf` then counts:
|
||||||
- For a request it allowed, the body bytes of the response, of the request
|
- For a request it allowed, the body bytes of the response, of the request
|
||||||
@@ -254,9 +252,9 @@ does not reach neighbours who did nothing. Requests refused under a ban are not
|
|||||||
counted against limits. They are counted in the client's history and in the
|
counted against limits. They are counted in the client's history and in the
|
||||||
ban's notes.
|
ban's notes.
|
||||||
|
|
||||||
A request over the header limits is refused with `414` or `431` before the bans
|
A request over the header limits is refused with `431` before the bans are
|
||||||
are checked (see "Answering in time"). It makes no ban, makes no ban permanent
|
checked (see "Answering in time"). It makes no ban, makes no ban permanent and
|
||||||
and is not counted in a ban's notes.
|
is not counted in a ban's notes.
|
||||||
|
|
||||||
An offence is a request `waf` holds against the client. It is one of:
|
An offence is a request `waf` holds against the client. It is one of:
|
||||||
|
|
||||||
@@ -1945,10 +1943,10 @@ leave out a cookie only when its name matches exactly, case included.
|
|||||||
### Ban lengths, response and scope
|
### Ban lengths, response and scope
|
||||||
|
|
||||||
`SWW_WAF_BAN_RESPONSE` is `403` by default, which makes a ban easy to recognise
|
`SWW_WAF_BAN_RESPONSE` is `403` by default, which makes a ban easy to recognise
|
||||||
when debugging; `429` and `close` are the alternatives. `close` does not leave
|
when debugging; `429` and `close` are the alternatives. Behind traefik, `close`
|
||||||
the client unanswered: the reverse proxy in front answers `502`, as it does
|
does not leave the client unanswered: traefik answers `502`, as it does whenever
|
||||||
whenever its backend drops a connection. `SWW_WAF_BAN_SCOPE_V4_PREFIX` widens an
|
its backend drops a connection. `SWW_WAF_BAN_SCOPE_V4_PREFIX` widens an IPv4 ban
|
||||||
IPv4 ban to the surrounding netblock. The four ban lengths are under "Bans".
|
to the surrounding netblock. The four ban lengths are under "Bans".
|
||||||
|
|
||||||
### Reputation
|
### Reputation
|
||||||
|
|
||||||
@@ -2375,15 +2373,15 @@ runs no check, writes no request log line and counts nothing in the metrics.
|
|||||||
answer but the last below carries `X-Sww-Disposition`.
|
answer but the last below carries `X-Sww-Disposition`.
|
||||||
|
|
||||||
- `X-Sww-Disposition: allow`, status `200`, no body. `rproxy` passes the request
|
- `X-Sww-Disposition: allow`, status `200`, no body. `rproxy` passes the request
|
||||||
to the app. While `SWW_WAF_ADD_LOOKUP_HEADERS` is `true`, the answer also
|
to the app. With `SWW_WAF_ADD_LOOKUP_HEADERS` the answer also carries
|
||||||
carries `X-Client-ASN` and `X-Client-Country`, which `rproxy` adds to the
|
`X-Client-ASN` and `X-Client-Country`, which `rproxy` adds to the request it
|
||||||
request it passes on. A header whose value is unknown is left out.
|
passes on. A header whose value is unknown is left out.
|
||||||
- `X-Sww-Disposition: deny`, with the status, headers and body the visitor is to
|
- `X-Sww-Disposition: deny`, with the status, headers and body the visitor is to
|
||||||
get. `rproxy` gives them to the visitor, less this header. The status is one
|
get. `rproxy` gives them to the visitor, less this header. The status is one
|
||||||
of:
|
of:
|
||||||
- `SWW_WAF_BAN_RESPONSE`, for a refusal;
|
- `SWW_WAF_BAN_RESPONSE`, for a refusal;
|
||||||
- `403`, for a `block` rule or the Core Rule Set;
|
- `403`, for a `block` rule or the Core Rule Set;
|
||||||
- `414` or `431`, for a request over the header limits;
|
- `431`, for a request over the header limits;
|
||||||
- `404`, for a path under `/_sww/` that passes the checks, so that the app
|
- `404`, for a path under `/_sww/` that passes the checks, so that the app
|
||||||
never sees that prefix.
|
never sees that prefix.
|
||||||
- `X-Sww-Disposition: close`, status `200`, no body. This takes the place of a
|
- `X-Sww-Disposition: close`, status `200`, no body. This takes the place of a
|
||||||
@@ -2444,31 +2442,22 @@ never wait. These can still wait for GeoJS:
|
|||||||
- a rule file;
|
- a rule file;
|
||||||
- the Core Rule Set.
|
- the Core Rule Set.
|
||||||
|
|
||||||
The header limits are fixed, with no setting. A line's length is in bytes and
|
The header limits are fixed, with no setting. `waf` refuses a request over
|
||||||
does not count its line ending. `waf` refuses a request whose request line is
|
either one:
|
||||||
longer than 8 KiB with `414`. An example is `GET /search?q=` followed by 30,000
|
|
||||||
characters.
|
|
||||||
|
|
||||||
It refuses a request with `431` for any of:
|
|
||||||
|
|
||||||
- a header line longer than 8 KiB. Each value of a header sent twice is a line
|
|
||||||
of its own. `Host` counts as a line.
|
|
||||||
- more than 50 cookies. Each part of its `Cookie` headers between semicolons
|
- more than 50 cookies. Each part of its `Cookie` headers between semicolons
|
||||||
counts as one, unless it is empty or only spaces and tabs.
|
counts as one, unless it is empty or only spaces and tabs.
|
||||||
- more than 100 header fields. A header sent twice counts twice. `Host` and
|
- more than 100 header fields. A header sent twice counts twice. `Host` and
|
||||||
`Transfer-Encoding` do not count. `X-Request-ID` and `X-Forwarded-Proto` do.
|
`Transfer-Encoding` do not count. `X-Request-ID` and `X-Forwarded-Proto` do.
|
||||||
|
|
||||||
Real clients rarely reach these limits. nginx refuses lines over 8 KiB by
|
Real clients never reach these limits. The answer is `deny` with `431`, and the
|
||||||
default, and Apache's default limit is about the same. The answer is `deny`. The
|
log line's action is `request_header_too_large`. The refusal makes no ban and is
|
||||||
log line's action is `request_line_too_long` for `414` and
|
no offence.
|
||||||
`request_header_too_large` for `431`. The refusal makes no ban and is no
|
|
||||||
offence.
|
|
||||||
|
|
||||||
The header limits are the first check after `SWW_WAF_ALLOW_NETS`. A request over
|
The header limits are the first check after `SWW_WAF_ALLOW_NETS`. A request over
|
||||||
them is refused before any later check runs. So it causes no lookup, no query to
|
them is refused before any later check runs. So it causes no lookup, no query to
|
||||||
an outside source and no rule match, and the rate limits do not count it. A
|
an outside source and no rule match, and the rate limits do not count it. A
|
||||||
client under a ban or in `SWW_WAF_DENY_NETS` also gets `414` or `431` for such a
|
client under a ban or in `SWW_WAF_DENY_NETS` also gets `431` for such a request.
|
||||||
request.
|
|
||||||
|
|
||||||
The limits apply on every path and in every `SWW_WAF_CORE_RULE_SET_MODE`. A
|
The limits apply on every path and in every `SWW_WAF_CORE_RULE_SET_MODE`. A
|
||||||
client in `SWW_WAF_ALLOW_NETS` skips them, as it skips the Core Rule Set. In
|
client in `SWW_WAF_ALLOW_NETS` skips them, as it skips the Core Rule Set. In
|
||||||
@@ -2476,10 +2465,9 @@ client in `SWW_WAF_ALLOW_NETS` skips them, as it skips the Core Rule Set. In
|
|||||||
other check runs on it.
|
other check runs on it.
|
||||||
|
|
||||||
The Core Rule Set runs each of its rules on each cookie, so thousands of small
|
The Core Rule Set runs each of its rules on each cookie, so thousands of small
|
||||||
cookies would keep it busy for over a second. One 30 KB cookie or query string
|
cookies would keep it busy for over a second. Within the limits, one long cookie
|
||||||
would keep it busy for a third of a second or more. A request with several long
|
or query string can still keep it busy for a fraction of a second. So enough
|
||||||
`Cookie` lines, each within the limits, can keep it as busy. Enough such
|
such requests at once can overload `waf` past
|
||||||
requests at once can still overload `waf` past
|
|
||||||
`SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS`.
|
`SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS`.
|
||||||
|
|
||||||
### The report
|
### The report
|
||||||
@@ -2773,10 +2761,9 @@ its whole body. The visitor gets that answer at once, and the connection closes
|
|||||||
after it.
|
after it.
|
||||||
|
|
||||||
The largest request line and headers is 32 KiB. A kept-open connection is closed
|
The largest request line and headers is 32 KiB. A kept-open connection is closed
|
||||||
after 120 seconds idle. Neither has a setting. The reverse proxy in front should
|
after 120 seconds idle. Neither has a setting. traefik, by default, closes an
|
||||||
close an unused connection sooner, so that it never sends a request on a
|
unused connection after 90 seconds. So traefik closes first, and never sends a
|
||||||
connection `rproxy` is closing. traefik closes one after 90 seconds by default.
|
request on a connection `rproxy` is closing.
|
||||||
nginx, by default, opens a new connection for each request.
|
|
||||||
|
|
||||||
What the visitor gets when a limit is passed:
|
What the visitor gets when a limit is passed:
|
||||||
|
|
||||||
@@ -2827,16 +2814,14 @@ chooses: `waf` runs `waf`, and with it unset the image runs `rproxy`.
|
|||||||
The usual setup is three containers on one private network:
|
The usual setup is three containers on one private network:
|
||||||
|
|
||||||
- the app;
|
- the app;
|
||||||
- `rproxy`, which the reverse proxy that terminates TLS forwards to, with
|
- `rproxy`, which traefik routes to, with `SWW_RPROXY_UPSTREAM_URL` naming the
|
||||||
`SWW_RPROXY_UPSTREAM_URL` naming the app and `SWW_RPROXY_WAF_BACKEND` naming
|
app and `SWW_RPROXY_WAF_BACKEND` naming `waf`;
|
||||||
`waf`;
|
|
||||||
- `waf`, with `SWW_COMMAND=waf` and a volume for its state files.
|
- `waf`, with `SWW_COMMAND=waf` and a volume for its state files.
|
||||||
|
|
||||||
Each further app adds its own container and its own `rproxy`, whose
|
Each further app adds its own container and its own `rproxy`, whose
|
||||||
`SWW_RPROXY_WAF_BACKEND` names the same `waf` (see "Sites"). Anything on that
|
`SWW_RPROXY_WAF_BACKEND` names the same `waf` (see "Sites"). Anything on that
|
||||||
network can reach an app around `rproxy`, and can ask `waf` questions or send it
|
network can reach an app around `rproxy`, and can ask `waf` questions or send it
|
||||||
reports. So the network holds only what you trust. "The reverse proxy in front"
|
reports. So the network holds only what you trust.
|
||||||
in `README.md` lists what that proxy must do.
|
|
||||||
|
|
||||||
### `rproxy` in the app's container
|
### `rproxy` in the app's container
|
||||||
|
|
||||||
@@ -2908,10 +2893,10 @@ process that stops at start is started again every second, not in a tight loop.
|
|||||||
container's output, which `docker logs` shows.
|
container's output, which `docker logs` shows.
|
||||||
- When a service exits, runit runs its `run` script again, and the container
|
- When a service exits, runit runs its `run` script again, and the container
|
||||||
does not stop. Beside an app, the other process keeps running.
|
does not stop. Beside an app, the other process keeps running.
|
||||||
- While `rproxy` is down, the reverse proxy in front answers `502`. While the
|
- While `rproxy` is down, traefik answers `502`. While the app is down, `rproxy`
|
||||||
app is down, `rproxy` answers `502`, which is not a client offence. While
|
answers `502`, which is not a client offence. While `waf` is down,
|
||||||
`waf` is down, `SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION` decides (see
|
`SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION` decides (see "Without an answer
|
||||||
"Without an answer from `waf`").
|
from `waf`").
|
||||||
- Only faults found at start stop `waf`:
|
- Only faults found at start stop `waf`:
|
||||||
- invalid configuration;
|
- invalid configuration;
|
||||||
- a lookup database that is missing or unreadable;
|
- a lookup database that is missing or unreadable;
|
||||||
@@ -2944,26 +2929,23 @@ checks:
|
|||||||
- With `SWW_COMMAND` unset, empty or `rproxy`, it passes while `rproxy` answers
|
- With `SWW_COMMAND` unset, empty or `rproxy`, it passes while `rproxy` answers
|
||||||
that request on `127.0.0.1`, at the port in `SWW_RPROXY_LISTEN_ADDR`, and the
|
that request on `127.0.0.1`, at the port in `SWW_RPROXY_LISTEN_ADDR`, and the
|
||||||
app accepts connections at `SWW_RPROXY_UPSTREAM_URL`. It does not ask `waf`,
|
app accepts connections at `SWW_RPROXY_UPSTREAM_URL`. It does not ask `waf`,
|
||||||
so that the site stays in the routes of the reverse proxy in front while `waf`
|
so that the site stays in traefik's routes while `waf` is down.
|
||||||
is down.
|
|
||||||
- With any other value, it fails with a message naming `SWW_COMMAND` and its
|
- With any other value, it fails with a message naming `SWW_COMMAND` and its
|
||||||
value.
|
value.
|
||||||
|
|
||||||
A reverse proxy that reads Docker's health status, as traefik does, sends a
|
traefik sends a container no requests until it shows as healthy. So the check
|
||||||
container no requests until it shows as healthy. So the check runs every second
|
runs every second from the container's start until it first passes, for up to a
|
||||||
from the container's start until it first passes, for up to a minute, and every
|
minute, and every 30 seconds after that. An app with a health check of its own
|
||||||
30 seconds after that. An app with a health check of its own can replace the
|
can replace the image's `HEALTHCHECK` with one that checks both.
|
||||||
image's `HEALTHCHECK` with one that checks both.
|
|
||||||
|
|
||||||
### Ports
|
### Ports
|
||||||
|
|
||||||
`rproxy` listens on no port but the one in `SWW_RPROXY_LISTEN_ADDR`, 8080 by
|
`rproxy` listens on no port but the one in `SWW_RPROXY_LISTEN_ADDR`, 8080 by
|
||||||
default, which the reverse proxy in front forwards to. It passes requests to the
|
default, which traefik routes to. It passes requests to the app at
|
||||||
app at `SWW_RPROXY_UPSTREAM_URL`, `http://127.0.0.1:8081` by default. Another
|
`SWW_RPROXY_UPSTREAM_URL`, `http://127.0.0.1:8081` by default. Another port may
|
||||||
port may be set: the health check follows the setting, and the reverse proxy in
|
be set: the health check follows the setting, and traefik's port label
|
||||||
front must name it, as traefik's port label
|
(`traefik.http.services.<name>.loadbalancer.server.port`) must name it. An app
|
||||||
(`traefik.http.services.<name>.loadbalancer.server.port`) does. An app beside
|
beside `rproxy` must leave that port free.
|
||||||
`rproxy` must leave that port free.
|
|
||||||
|
|
||||||
`waf` listens on no port but the one in `SWW_WAF_LISTEN_ADDR`, 8080 by default.
|
`waf` listens on no port but the one in `SWW_WAF_LISTEN_ADDR`, 8080 by default.
|
||||||
The questions, the reports and its own endpoints all come there.
|
The questions, the reports and its own endpoints all come there.
|
||||||
@@ -3004,9 +2986,9 @@ in `docs/settings.md` shows the commands.
|
|||||||
### Forwarded headers
|
### Forwarded headers
|
||||||
|
|
||||||
The app's TCP peer is `rproxy`, and the `X-Forwarded-For` the app receives ends
|
The app's TCP peer is `rproxy`, and the `X-Forwarded-For` the app receives ends
|
||||||
with the address of the reverse proxy in front, which `rproxy` adds after the
|
with traefik's address, which `rproxy` adds after the visitor's. So the app must
|
||||||
visitor's. So the app must trust `rproxy`'s address for forwarded headers,
|
trust `rproxy`'s address for forwarded headers, besides the private address
|
||||||
besides the private address ranges that proxy is on.
|
ranges traefik is on.
|
||||||
|
|
||||||
- In a container of its own, `rproxy`'s address is on the private network. The
|
- In a container of its own, `rproxy`'s address is on the private network. The
|
||||||
usual default for a trusted-proxy setting, the private ranges, covers it.
|
usual default for a trusted-proxy setting, the private ranges, covers it.
|
||||||
|
|||||||
+3
-8
@@ -68,8 +68,7 @@ Every one of these lines has:
|
|||||||
### Request detail
|
### Request detail
|
||||||
|
|
||||||
- `request_id`: the id `rproxy` made for the request when it arrived.
|
- `request_id`: the id `rproxy` made for the request when it arrived.
|
||||||
- `peer_ip`: the address of `rproxy`'s TCP peer, normally the reverse proxy in
|
- `peer_ip`: the address of `rproxy`'s TCP peer, normally traefik.
|
||||||
front of it.
|
|
||||||
- `forwarded_for`: the `X-Forwarded-For` header the visitor's request came with.
|
- `forwarded_for`: the `X-Forwarded-For` header the visitor's request came with.
|
||||||
- `client_group`: the client as rate limiting counts it, an IPv4 address as a
|
- `client_group`: the client as rate limiting counts it, an IPv4 address as a
|
||||||
/32 or an IPv6 group.
|
/32 or an IPv6 group.
|
||||||
@@ -103,8 +102,7 @@ Every one of these lines has:
|
|||||||
ban this request made or made permanent;
|
ban this request made or made permanent;
|
||||||
- `country_denied`, `client_is_rate_limited`, `blocked_by_block_rule` and
|
- `country_denied`, `client_is_rate_limited`, `blocked_by_block_rule` and
|
||||||
`blocked_by_core_rule_set`: refused by that check;
|
`blocked_by_core_rule_set`: refused by that check;
|
||||||
- `request_line_too_long`: a request line over the header limits;
|
- `request_header_too_large`: over a header limit;
|
||||||
- `request_header_too_large`: headers over the header limits;
|
|
||||||
- `own_endpoint`: answered at one of `waf`'s own endpoints, or `404` for a
|
- `own_endpoint`: answered at one of `waf`'s own endpoints, or `404` for a
|
||||||
path under `/_sww/`.
|
path under `/_sww/`.
|
||||||
- `would_action`: in `observe` mode, what `enforce` mode would have done with a
|
- `would_action`: in `observe` mode, what `enforce` mode would have done with a
|
||||||
@@ -126,10 +124,7 @@ Every one of these lines has:
|
|||||||
- `rate_limits`: `window`, `count` and `limit`;
|
- `rate_limits`: `window`, `count` and `limit`;
|
||||||
- `trap_paths`: `path`, the trap path asked for;
|
- `trap_paths`: `path`, the trap path asked for;
|
||||||
- `rule_files`: `rule_id` and `rule_action`, of the rule that refused it;
|
- `rule_files`: `rule_id` and `rule_action`, of the rule that refused it;
|
||||||
- `header_limits`: for a request line, `request_line_bytes` and
|
- `header_limits`: `fields`, `fields_limit`, `cookies` and `cookies_limit`;
|
||||||
`request_line_bytes_limit`; for headers, `longest_header_line_bytes`,
|
|
||||||
`header_line_bytes_limit`, `fields`, `fields_limit`, `cookies` and
|
|
||||||
`cookies_limit`;
|
|
||||||
- `core_rule_set`: `score`, and `threshold`,
|
- `core_rule_set`: `score`, and `threshold`,
|
||||||
`SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD`;
|
`SWW_WAF_CORE_RULE_SET_SCORE_THRESHOLD`;
|
||||||
- `refusal_limit`: `window`, `count` and `limit`, of the refusal limit;
|
- `refusal_limit`: `window`, `count` and `limit`, of the refusal limit;
|
||||||
|
|||||||
+8
-11
@@ -150,7 +150,7 @@ cut off; the client is banned.
|
|||||||
- `SWW_WAF_LOOKUP_SOURCE` (default `geojs`): `geojs`, `file` or `off`. `off`
|
- `SWW_WAF_LOOKUP_SOURCE` (default `geojs`): `geojs`, `file` or `off`. `off`
|
||||||
stops the start while a setting that needs a lookup is set:
|
stops the start while a setting that needs a lookup is set:
|
||||||
- `SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES`;
|
- `SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES`;
|
||||||
- `SWW_WAF_ADD_LOOKUP_HEADERS` set to `true`;
|
- `SWW_WAF_ADD_LOOKUP_HEADERS`;
|
||||||
- `SWW_WAF_TAG_DENY` or `SWW_WAF_TAG_LIMIT_PERCENT` naming a `country=` tag,
|
- `SWW_WAF_TAG_DENY` or `SWW_WAF_TAG_LIMIT_PERCENT` naming a `country=` tag,
|
||||||
or an `asn=` tag while `SWW_WAF_ROUTEWATCH_URL` is unset;
|
or an `asn=` tag while `SWW_WAF_ROUTEWATCH_URL` is unset;
|
||||||
- the two byte percentage lists and `SWW_WAF_ASN_LIMIT_PERCENT_URL`;
|
- the two byte percentage lists and `SWW_WAF_ASN_LIMIT_PERCENT_URL`;
|
||||||
@@ -161,11 +161,9 @@ cut off; the client is banned.
|
|||||||
- `SWW_WAF_ASN_AND_COUNTRY_LOOKUP_TIMEOUT_MS` (default `800`): how long a
|
- `SWW_WAF_ASN_AND_COUNTRY_LOOKUP_TIMEOUT_MS` (default `800`): how long a
|
||||||
request waits for its client's first answer from GeoJS, and how long a request
|
request waits for its client's first answer from GeoJS, and how long a request
|
||||||
to GeoJS may take. A time in milliseconds; never `off`.
|
to GeoJS may take. A time in milliseconds; never `off`.
|
||||||
- `SWW_WAF_ADD_LOOKUP_HEADERS` (default `true`): `true` has `rproxy` pass the
|
- `SWW_WAF_ADD_LOOKUP_HEADERS` (default `false`): `true` has `rproxy` pass the
|
||||||
app the client's AS number in `X-Client-ASN` and its country in
|
app the client's AS number in `X-Client-ASN` and its country in
|
||||||
`X-Client-Country`. The default is `false` while `SWW_WAF_LOOKUP_SOURCE` is
|
`X-Client-Country`.
|
||||||
`off`. With GeoJS, a new client's first request waits for its answer, up to
|
|
||||||
`SWW_WAF_ASN_AND_COUNTRY_LOOKUP_TIMEOUT_MS`.
|
|
||||||
|
|
||||||
## Country list
|
## Country list
|
||||||
|
|
||||||
@@ -423,9 +421,8 @@ A few limits are fixed rather than settings:
|
|||||||
- at most 20,000 anomaly counters are kept;
|
- at most 20,000 anomaly counters are kept;
|
||||||
- each alert destination's queue holds at most 1,000 alerts;
|
- each alert destination's queue holds at most 1,000 alerts;
|
||||||
- `waf` holds at most 10,000 answered requests awaiting their reports;
|
- `waf` holds at most 10,000 answered requests awaiting their reports;
|
||||||
- `waf` refuses a request with a request line or header line over 8 KiB, more
|
- `waf` refuses a request with more than 50 cookies or more than 100 header
|
||||||
than 50 cookies or more than 100 header fields (see "Answering in time" under
|
fields (see "Answering in time" under "`waf`" in [`SPEC.md`](../SPEC.md));
|
||||||
"`waf`" in [`SPEC.md`](../SPEC.md));
|
|
||||||
- `rproxy` takes a request line and headers of at most 32 KiB, and closes a
|
- `rproxy` takes a request line and headers of at most 32 KiB, and closes a
|
||||||
kept-open connection idle for 120 seconds;
|
kept-open connection idle for 120 seconds;
|
||||||
- `rproxy` reads at most 1 MiB of the body of `waf`'s answer, and answers `502`
|
- `rproxy` reads at most 1 MiB of the body of `waf`'s answer, and answers `502`
|
||||||
@@ -478,9 +475,9 @@ start, and an invalid value stops the start. Any can be given as a file, as a
|
|||||||
`10.0.0.0/8,172.16.0.0/12,192.168.0.0/16`): the netblocks whose
|
`10.0.0.0/8,172.16.0.0/12,192.168.0.0/16`): the netblocks whose
|
||||||
`X-Forwarded-For` is believed. Set but empty, it trusts nothing. A client
|
`X-Forwarded-For` is believed. Set but empty, it trusts nothing. A client
|
||||||
outside these netblocks cannot claim another address. Where other containers
|
outside these netblocks cannot claim another address. Where other containers
|
||||||
reach `rproxy` directly, or visitors on a private network reach the reverse
|
reach `rproxy` directly, or visitors on a private network reach traefik, they
|
||||||
proxy in front, they can name another address in the header. Narrowing the
|
can name another address in the header. Narrowing the setting to traefik's own
|
||||||
setting to that proxy's own network closes that gap.
|
network closes that gap.
|
||||||
|
|
||||||
## The image's command
|
## The image's command
|
||||||
|
|
||||||
|
|||||||
@@ -84,8 +84,7 @@ type WAF struct {
|
|||||||
// client's first answer from GeoJS while a setting needs it
|
// client's first answer from GeoJS while a setting needs it
|
||||||
// (SWW_WAF_ASN_AND_COUNTRY_LOOKUP_TIMEOUT_MS), which cannot be off.
|
// (SWW_WAF_ASN_AND_COUNTRY_LOOKUP_TIMEOUT_MS), which cannot be off.
|
||||||
// AddLookupHeaders is true when the app is passed the client's AS number
|
// AddLookupHeaders is true when the app is passed the client's AS number
|
||||||
// and country in headers (SWW_WAF_ADD_LOOKUP_HEADERS), by default unless
|
// and country in headers (SWW_WAF_ADD_LOOKUP_HEADERS).
|
||||||
// LookupSource is off.
|
|
||||||
LookupSource string
|
LookupSource string
|
||||||
LookupDBPath string
|
LookupDBPath string
|
||||||
ASNAndCountryLookupTimeout time.Duration
|
ASNAndCountryLookupTimeout time.Duration
|
||||||
@@ -550,6 +549,7 @@ func WAFFromEnvironment(v *viper.Viper) (*WAF, error) {
|
|||||||
"SWW_WAF_ASN_AND_COUNTRY_LOOKUP_TIMEOUT_MS",
|
"SWW_WAF_ASN_AND_COUNTRY_LOOKUP_TIMEOUT_MS",
|
||||||
"800",
|
"800",
|
||||||
),
|
),
|
||||||
|
AddLookupHeaders: env.boolean("SWW_WAF_ADD_LOOKUP_HEADERS", "false"),
|
||||||
ExclusivelyAllowedCountries: env.countries(
|
ExclusivelyAllowedCountries: env.countries(
|
||||||
"SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES", ""),
|
"SWW_WAF_EXCLUSIVELY_ALLOWED_COUNTRIES", ""),
|
||||||
TagLists: env.tagLists("SWW_WAF_TAG_LISTS"),
|
TagLists: env.tagLists("SWW_WAF_TAG_LISTS"),
|
||||||
@@ -724,9 +724,6 @@ func WAFFromEnvironment(v *viper.Viper) (*WAF, error) {
|
|||||||
|
|
||||||
cfg.LogRemoteAppName = env.appName("SWW_WAF_LOG_REMOTE_APP_NAME",
|
cfg.LogRemoteAppName = env.appName("SWW_WAF_LOG_REMOTE_APP_NAME",
|
||||||
cfg.InstanceName, cfg.LogRemoteURL != nil)
|
cfg.InstanceName, cfg.LogRemoteURL != nil)
|
||||||
// The app gets the headers by default, unless no client is looked up.
|
|
||||||
cfg.AddLookupHeaders = env.boolean("SWW_WAF_ADD_LOOKUP_HEADERS",
|
|
||||||
strconv.FormatBool(cfg.LookupSource != off))
|
|
||||||
cfg.BlocklistAction, cfg.BlocklistLimitPercent = env.action(
|
cfg.BlocklistAction, cfg.BlocklistLimitPercent = env.action(
|
||||||
"SWW_WAF_BLOCKLIST_ACTION", "deny")
|
"SWW_WAF_BLOCKLIST_ACTION", "deny")
|
||||||
cfg.DNSBLAbuseIPDBAction, cfg.DNSBLAbuseIPDBLimitPercent = env.action(
|
cfg.DNSBLAbuseIPDBAction, cfg.DNSBLAbuseIPDBLimitPercent = env.action(
|
||||||
|
|||||||
@@ -287,7 +287,6 @@ func TestDefaults(t *testing.T) {
|
|||||||
})
|
})
|
||||||
wantLookupSettings(t, cfg, config.WAF{
|
wantLookupSettings(t, cfg, config.WAF{
|
||||||
LookupSource: defaultLookupSource, ASNAndCountryLookupTimeout: 800 * time.Millisecond,
|
LookupSource: defaultLookupSource, ASNAndCountryLookupTimeout: 800 * time.Millisecond,
|
||||||
AddLookupHeaders: true,
|
|
||||||
})
|
})
|
||||||
wantByteLimitSettings(t, cfg, config.WAF{
|
wantByteLimitSettings(t, cfg, config.WAF{
|
||||||
BytesLimitPerMinute: 0, BytesLimitPerHour: 50 << 30,
|
BytesLimitPerMinute: 0, BytesLimitPerHour: 50 << 30,
|
||||||
@@ -1396,13 +1395,13 @@ func TestLookupSettingsAsSet(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cfg := wafFromEnvironment(t, environment{
|
cfg := wafFromEnvironment(t, environment{
|
||||||
asnAndCountryLookupTimeout: "500", addLookupHeaders: disabled,
|
asnAndCountryLookupTimeout: "500", addLookupHeaders: enabled,
|
||||||
})
|
})
|
||||||
wantLookupSettings(t, cfg, config.WAF{
|
wantLookupSettings(t, cfg, config.WAF{
|
||||||
LookupSource: defaultLookupSource, ASNAndCountryLookupTimeout: 500 * time.Millisecond,
|
LookupSource: defaultLookupSource, ASNAndCountryLookupTimeout: 500 * time.Millisecond,
|
||||||
|
AddLookupHeaders: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
// With no client looked up, the headers are off unless set.
|
|
||||||
cfg = wafFromEnvironment(t, environment{lookupSource: off})
|
cfg = wafFromEnvironment(t, environment{lookupSource: off})
|
||||||
wantLookupSettings(t, cfg, config.WAF{
|
wantLookupSettings(t, cfg, config.WAF{
|
||||||
LookupSource: off, ASNAndCountryLookupTimeout: 800 * time.Millisecond,
|
LookupSource: off, ASNAndCountryLookupTimeout: 800 * time.Millisecond,
|
||||||
@@ -1423,10 +1422,6 @@ func TestLookupDBPathGoesWithTheFileSourceAlone(t *testing.T) {
|
|||||||
cfg.LookupSource, cfg.LookupDBPath, path)
|
cfg.LookupSource, cfg.LookupDBPath, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cfg.AddLookupHeaders {
|
|
||||||
t.Error("headers off by default with lookups from file, want on")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
env environment
|
env environment
|
||||||
want string
|
want string
|
||||||
@@ -2932,7 +2927,7 @@ func TestLogsEachSettingWithItsValue(t *testing.T) {
|
|||||||
lookupSource: defaultLookupSource,
|
lookupSource: defaultLookupSource,
|
||||||
lookupDBPath: "",
|
lookupDBPath: "",
|
||||||
asnAndCountryLookupTimeout: "800",
|
asnAndCountryLookupTimeout: "800",
|
||||||
addLookupHeaders: enabled,
|
addLookupHeaders: disabled,
|
||||||
allowedCountries: "",
|
allowedCountries: "",
|
||||||
tagLists: "",
|
tagLists: "",
|
||||||
tagDeny: "",
|
tagDeny: "",
|
||||||
|
|||||||
@@ -21,12 +21,7 @@ const (
|
|||||||
// the app.
|
// the app.
|
||||||
ActionForward = "forward"
|
ActionForward = "forward"
|
||||||
|
|
||||||
// ActionRequestLineTooLong is a request whose request line is too long for
|
// ActionRequestHeaderTooLarge is a request over the header limits.
|
||||||
// the header limits.
|
|
||||||
ActionRequestLineTooLong = "request_line_too_long"
|
|
||||||
|
|
||||||
// ActionRequestHeaderTooLarge is a request whose headers are over the
|
|
||||||
// header limits.
|
|
||||||
ActionRequestHeaderTooLarge = "request_header_too_large"
|
ActionRequestHeaderTooLarge = "request_header_too_large"
|
||||||
|
|
||||||
// ActionClientIsRateLimited is a request refused because it took its
|
// ActionClientIsRateLimited is a request refused because it took its
|
||||||
@@ -148,7 +143,7 @@ type Line struct {
|
|||||||
// taken with a request it would have refused: ActionClientDenied,
|
// taken with a request it would have refused: ActionClientDenied,
|
||||||
// ActionTemporarilyBanned, ActionPermanentlyBanned, ActionCountryDenied,
|
// ActionTemporarilyBanned, ActionPermanentlyBanned, ActionCountryDenied,
|
||||||
// ActionClientIsRateLimited, ActionBlockedByBlockRule,
|
// ActionClientIsRateLimited, ActionBlockedByBlockRule,
|
||||||
// ActionBlockedByCoreRuleSet, or, in waf, ActionRequestLineTooLong or
|
// ActionBlockedByCoreRuleSet, or, in waf,
|
||||||
// ActionRequestHeaderTooLarge for the header limits.
|
// ActionRequestHeaderTooLarge for the header limits.
|
||||||
WouldAction string `json:"would_action,omitempty"`
|
WouldAction string `json:"would_action,omitempty"`
|
||||||
// Heuristic is the name of the heuristic that refused the request, in
|
// Heuristic is the name of the heuristic that refused the request, in
|
||||||
|
|||||||
@@ -349,7 +349,6 @@ func TestRequestCountsForTheASNumberGeoJSGivesBeforeItEnds(t *testing.T) {
|
|||||||
map[string]string{
|
map[string]string{
|
||||||
trustedProxies: trustLocalhost,
|
trustedProxies: trustLocalhost,
|
||||||
asnAndCountryLookupTimeout: "3600000",
|
asnAndCountryLookupTimeout: "3600000",
|
||||||
addLookupHeaders: falseValue,
|
|
||||||
anomalyASN + requestsPerMinute: aLot,
|
anomalyASN + requestsPerMinute: aLot,
|
||||||
})
|
})
|
||||||
server.Store(started)
|
server.Store(started)
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ const (
|
|||||||
// The header limits SPEC.md gives.
|
// The header limits SPEC.md gives.
|
||||||
cookieLimit = 50
|
cookieLimit = 50
|
||||||
headerFieldLimit = 100
|
headerFieldLimit = 100
|
||||||
lineLimit = 8192
|
|
||||||
// cookieHeader is the header the cookie limit counts.
|
// cookieHeader is the header the cookie limit counts.
|
||||||
cookieHeader = "Cookie"
|
cookieHeader = "Cookie"
|
||||||
// tooLarge is how waf refuses a request over the header limits.
|
// tooLarge is how waf refuses a request over the header limits.
|
||||||
@@ -105,139 +104,11 @@ func TestHeaderLimitRefusalNamesItsHeuristicCountsAndLimits(t *testing.T) {
|
|||||||
fillHeaders(req, headerFieldLimit+1)
|
fillHeaders(req, headerFieldLimit+1)
|
||||||
do(t, req)
|
do(t, req)
|
||||||
|
|
||||||
// The longest header line is X-Request-ID's, with its 26-character id.
|
|
||||||
wantHeuristic(
|
wantHeuristic(
|
||||||
t,
|
t,
|
||||||
s.out.requestLine(t),
|
s.out.requestLine(t),
|
||||||
"header_limits",
|
"header_limits",
|
||||||
`{"longest_header_line_bytes": 40, "header_line_bytes_limit": 8192,
|
`{"fields": 101, "fields_limit": 100, "cookies": 0, "cookies_limit": 50}`,
|
||||||
"fields": 101, "fields_limit": 100, "cookies": 0, "cookies_limit": 50}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// longQuery returns a path whose request line, sent by Go's client as
|
|
||||||
// "GET <path> HTTP/1.1", is lineBytes long.
|
|
||||||
func longQuery(lineBytes int) string {
|
|
||||||
const around = len("GET /?q= HTTP/1.1")
|
|
||||||
|
|
||||||
return "/?q=" + strings.Repeat("a", lineBytes-around)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestARequestLineOverTheLimitIsRefusedWith414BeforeAnyOtherCheck(
|
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
for _, tc := range []struct {
|
|
||||||
name string
|
|
||||||
lineBytes int
|
|
||||||
// cookies is how many cookies the request carries.
|
|
||||||
cookies int
|
|
||||||
status int
|
|
||||||
action string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
"at the limit", lineLimit, 0, http.StatusOK,
|
|
||||||
requestlog.ActionForward,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"over the limit", lineLimit + 1, 0, http.StatusRequestURITooLong,
|
|
||||||
requestlog.ActionRequestLineTooLong,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"over the limit and the cookie limit", lineLimit + 1,
|
|
||||||
cookieLimit + 1, http.StatusRequestURITooLong,
|
|
||||||
requestlog.ActionRequestLineTooLong,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
s := startWAF(t, "", map[string]string{coreRuleSetMode: block})
|
|
||||||
req := s.question(siteA, client, longQuery(tc.lineBytes))
|
|
||||||
|
|
||||||
if tc.cookies > 0 {
|
|
||||||
req.Header.Set(cookieHeader, cookies(tc.cookies))
|
|
||||||
}
|
|
||||||
|
|
||||||
got := do(t, req)
|
|
||||||
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},
|
|
||||||
{"a header sent twice, each line within the limit", func(req *http.Request) {
|
|
||||||
req.Header.Add("X-Long", strings.Repeat("a", 8000))
|
|
||||||
req.Header.Add("X-Long", strings.Repeat("a", 8000))
|
|
||||||
}, false},
|
|
||||||
} {
|
|
||||||
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}`,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -387,14 +387,12 @@ func TestRequestWaitsForItsLookupWhileABiasedThresholdIsSet(t *testing.T) {
|
|||||||
|
|
||||||
// In a synctest bubble, as
|
// In a synctest bubble, as
|
||||||
// TestRequestWaitsAsLongAsTheASNAndCountryLookupTimeoutSays says,
|
// TestRequestWaitsAsLongAsTheASNAndCountryLookupTimeoutSays says,
|
||||||
// with a GeoJS that never answers. The headers, which need the
|
// with a GeoJS that never answers.
|
||||||
// answer too, are off.
|
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
server, out, _ := newWAF(t, silentGeoJSURL, time.Now,
|
server, out, _ := newWAF(t, silentGeoJSURL, time.Now,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
asnAndCountryLookupTimeout: strconv.FormatInt(
|
asnAndCountryLookupTimeout: strconv.FormatInt(
|
||||||
timeout.Milliseconds(), 10),
|
timeout.Milliseconds(), 10),
|
||||||
addLookupHeaders: falseValue,
|
|
||||||
tagLists: "vpn=" + vpnList,
|
tagLists: "vpn=" + vpnList,
|
||||||
tc.setting: tc.value,
|
tc.setting: tc.value,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -108,11 +108,8 @@ func TestCountryRefusalNamesItsHeuristicCountryAndSetting(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
geojsURL, _ := startGeoJS(t)
|
geojsURL, _ := startGeoJS(t)
|
||||||
// The headers are off, so that only the country list makes the request
|
|
||||||
// wait for its answer.
|
|
||||||
s, _, _ := startWithClock(t, geojsURL, map[string]string{
|
s, _, _ := startWithClock(t, geojsURL, map[string]string{
|
||||||
asnAndCountryLookupTimeout: "3600000", addLookupHeaders: falseValue,
|
asnAndCountryLookupTimeout: "3600000", allowedCountries: "de",
|
||||||
allowedCountries: "de",
|
|
||||||
})
|
})
|
||||||
|
|
||||||
line := s.get(
|
line := s.get(
|
||||||
@@ -242,10 +239,7 @@ func TestPrivateAddressIsNeverLookedUp(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
env map[string]string
|
env map[string]string
|
||||||
}{
|
}{
|
||||||
{
|
{"no setting needs the lookup", nil},
|
||||||
"no setting needs the lookup",
|
|
||||||
map[string]string{addLookupHeaders: falseValue},
|
|
||||||
},
|
|
||||||
{"a tag setting needs it", map[string]string{tagDeny: denyKP}},
|
{"a tag setting needs it", map[string]string{tagDeny: denyKP}},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
|||||||
@@ -10,22 +10,16 @@ import (
|
|||||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The header limits: the most cookies and header fields a request may have,
|
// The header limits: the most cookies and header fields a request may have.
|
||||||
// and the longest its request line and each header line may be, in bytes, not
|
// Real clients never reach them.
|
||||||
// counting the line ending. Real clients rarely reach them. maxLineBytes is
|
|
||||||
// what nginx allows by default.
|
|
||||||
const (
|
const (
|
||||||
maxCookies = 50
|
maxCookies = 50
|
||||||
maxHeaderFields = 100
|
maxHeaderFields = 100
|
||||||
maxLineBytes = 8 << 10
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// headerLimits refuses a request with a request line longer than maxLineBytes,
|
// headerLimits refuses a request with more than maxHeaderFields header fields
|
||||||
// with 414. It refuses a request with a header line longer than maxLineBytes,
|
// or more than maxCookies cookies, with 431. Its figures are the request's
|
||||||
// more than maxHeaderFields header fields or more than maxCookies cookies, with
|
// header fields and cookies, and the limit of each.
|
||||||
// 431. Its figures are the request line's length and its limit for 414. For
|
|
||||||
// 431 they are the longest header line, the header fields and the cookies,
|
|
||||||
// each with its limit.
|
|
||||||
type headerLimits struct{}
|
type headerLimits struct{}
|
||||||
|
|
||||||
func (headerLimits) name() string { return "header_limits" }
|
func (headerLimits) name() string { return "header_limits" }
|
||||||
@@ -35,59 +29,20 @@ func (headerLimits) check(
|
|||||||
rq *request,
|
rq *request,
|
||||||
_ time.Time,
|
_ time.Time,
|
||||||
) *verdict {
|
) *verdict {
|
||||||
requestLine := rq.requestLineBytes()
|
|
||||||
if requestLine > maxLineBytes {
|
|
||||||
return &verdict{
|
|
||||||
action: requestlog.ActionRequestLineTooLong,
|
|
||||||
figures: map[string]any{
|
|
||||||
"request_line_bytes": requestLine,
|
|
||||||
"request_line_bytes_limit": maxLineBytes,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
longest := rq.longestHeaderLineBytes()
|
|
||||||
fields, cookies := rq.headerCounts()
|
fields, cookies := rq.headerCounts()
|
||||||
|
if fields <= maxHeaderFields && cookies <= maxCookies {
|
||||||
if longest <= maxLineBytes && fields <= maxHeaderFields &&
|
|
||||||
cookies <= maxCookies {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &verdict{
|
return &verdict{
|
||||||
action: requestlog.ActionRequestHeaderTooLarge,
|
action: requestlog.ActionRequestHeaderTooLarge,
|
||||||
figures: map[string]any{
|
figures: map[string]any{
|
||||||
"longest_header_line_bytes": longest,
|
|
||||||
"header_line_bytes_limit": maxLineBytes,
|
|
||||||
"fields": fields, "fields_limit": maxHeaderFields,
|
"fields": fields, "fields_limit": maxHeaderFields,
|
||||||
"cookies": cookies, "cookies_limit": maxCookies,
|
"cookies": cookies, "cookies_limit": maxCookies,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// requestLineBytes returns the length of the request's request line, such as
|
|
||||||
// "GET /search?q=x HTTP/1.1", not counting its line ending. The target is the
|
|
||||||
// one the visitor sent, which rproxy's question carries unchanged.
|
|
||||||
func (rq *request) requestLineBytes() int {
|
|
||||||
return len(rq.in.Method) + len(" ") + len(rq.in.RequestURI) +
|
|
||||||
len(" ") + len(rq.in.Proto)
|
|
||||||
}
|
|
||||||
|
|
||||||
// longestHeaderLineBytes returns the length of the request's longest header
|
|
||||||
// line, such as "Cookie: a=1", not counting its line ending. Each value of a
|
|
||||||
// header sent twice is a line of its own, and Host is a line too.
|
|
||||||
func (rq *request) longestHeaderLineBytes() int {
|
|
||||||
longest := len("Host: ") + len(rq.in.Host)
|
|
||||||
|
|
||||||
for name, values := range rq.in.Header {
|
|
||||||
for _, value := range values {
|
|
||||||
longest = max(longest, len(name)+len(": ")+len(value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return longest
|
|
||||||
}
|
|
||||||
|
|
||||||
// headerCounts returns the request's header fields, each value of a header sent
|
// headerCounts returns the request's header fields, each value of a header sent
|
||||||
// twice being one, and its cookies, each part of a Cookie header between
|
// twice being one, and its cookies, each part of a Cookie header between
|
||||||
// semicolons being one unless it is empty or only spaces and tabs. The Core
|
// semicolons being one unless it is empty or only spaces and tabs. The Core
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ const (
|
|||||||
|
|
||||||
// beforeLookup are the heuristics that run before the client's AS number and
|
// beforeLookup are the heuristics that run before the client's AS number and
|
||||||
// country are looked up, so that a client they refuse is not looked up.
|
// country are looked up, so that a client they refuse is not looked up.
|
||||||
// header_limits comes first: it only measures and counts, and a request it
|
// header_limits comes first: it only counts, and a request it refuses causes no
|
||||||
// refuses causes no other work.
|
// other work.
|
||||||
func beforeLookup() []heuristic {
|
func beforeLookup() []heuristic {
|
||||||
return []heuristic{
|
return []heuristic{
|
||||||
headerLimits{},
|
headerLimits{},
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// The headers in which an answer of allow gives rproxy the client's AS number
|
// The headers in which an answer of allow gives rproxy the client's AS number
|
||||||
// and country, for the app, while SWW_WAF_ADD_LOOKUP_HEADERS is true. Go writes
|
// and country, for the app, while SWW_WAF_ADD_LOOKUP_HEADERS is set. Go writes
|
||||||
// every header name in this form, as it sends it and as it receives it, so
|
// every header name in this form, as it sends it and as it receives it, so
|
||||||
// X-Client-ASN arrives as X-Client-Asn; header names are not case-sensitive.
|
// X-Client-ASN arrives as X-Client-Asn; header names are not case-sensitive.
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ func TestEveryClientIsLookedUpWithoutWaitingWhileNoSettingNeedsIt(
|
|||||||
geojsURL, asked, release := startHeldGeoJS(t)
|
geojsURL, asked, release := startHeldGeoJS(t)
|
||||||
s, _, server := startWithClock(t, geojsURL, map[string]string{
|
s, _, server := startWithClock(t, geojsURL, map[string]string{
|
||||||
asnAndCountryLookupTimeout: "3600000",
|
asnAndCountryLookupTimeout: "3600000",
|
||||||
addLookupHeaders: falseValue,
|
|
||||||
rateLimitPerMinute: "1",
|
rateLimitPerMinute: "1",
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -419,7 +418,7 @@ func TestClientsOwnLookupHeadersAreRemovedWhileTheSettingIsOff(
|
|||||||
geojsURL,
|
geojsURL,
|
||||||
time.Now,
|
time.Now,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
trustedProxies: trustLocalhost, addLookupHeaders: falseValue,
|
trustedProxies: trustLocalhost,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
s := &sender{t: t, addr: addr, out: out}
|
s := &sender{t: t, addr: addr, out: out}
|
||||||
@@ -458,8 +457,6 @@ func TestRequestWaitsAsLongAsTheASNAndCountryLookupTimeoutSays(
|
|||||||
map[string]string{
|
map[string]string{
|
||||||
asnAndCountryLookupTimeout: strconv.FormatInt(
|
asnAndCountryLookupTimeout: strconv.FormatInt(
|
||||||
timeout.Milliseconds(), 10),
|
timeout.Milliseconds(), 10),
|
||||||
// Off, so that only the country list makes the request wait.
|
|
||||||
addLookupHeaders: falseValue,
|
|
||||||
allowedCountries: "DE",
|
allowedCountries: "DE",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -93,10 +93,6 @@ const off = "off"
|
|||||||
// on is the value that switches a setting that takes true or false on.
|
// on is the value that switches a setting that takes true or false on.
|
||||||
const on = "true"
|
const on = "true"
|
||||||
|
|
||||||
// falseValue is the value that switches a setting that takes true or false
|
|
||||||
// off.
|
|
||||||
const falseValue = "false"
|
|
||||||
|
|
||||||
// output collects what sww writes on stdout.
|
// output collects what sww writes on stdout.
|
||||||
type output struct {
|
type output struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ func requestHeaders(r *http.Request, names []string) map[string]string {
|
|||||||
// check is the one place where a request can be refused. It returns nil to let
|
// check is the one place where a request can be refused. It returns nil to let
|
||||||
// the request through. The heuristics of checkClient decide: a request they
|
// the request through. The heuristics of checkClient decide: a request they
|
||||||
// refuse is answered with SWW_WAF_BAN_RESPONSE, 403 for a block rule or the Core
|
// refuse is answered with SWW_WAF_BAN_RESPONSE, 403 for a block rule or the Core
|
||||||
// Rule Set, or 414 or 431 for the header limits. In observe mode nothing is refused,
|
// Rule Set, or 431 for the header limits. In observe mode nothing is refused,
|
||||||
// and the log line names what enforce mode would have done. ctx is the
|
// and the log line names what enforce mode would have done. ctx is the
|
||||||
// request's own context.
|
// request's own context.
|
||||||
func (rq *request) check(ctx context.Context) *refusal {
|
func (rq *request) check(ctx context.Context) *refusal {
|
||||||
@@ -196,8 +196,6 @@ func (rq *request) check(ctx context.Context) *refusal {
|
|||||||
case action == requestlog.ActionBlockedByBlockRule ||
|
case action == requestlog.ActionBlockedByBlockRule ||
|
||||||
action == requestlog.ActionBlockedByCoreRuleSet:
|
action == requestlog.ActionBlockedByCoreRuleSet:
|
||||||
return &refusal{status: http.StatusForbidden, action: action}
|
return &refusal{status: http.StatusForbidden, action: action}
|
||||||
case action == requestlog.ActionRequestLineTooLong:
|
|
||||||
return &refusal{status: http.StatusRequestURITooLong, action: action}
|
|
||||||
case action == requestlog.ActionRequestHeaderTooLarge:
|
case action == requestlog.ActionRequestHeaderTooLarge:
|
||||||
return &refusal{
|
return &refusal{
|
||||||
status: http.StatusRequestHeaderFieldsTooLarge,
|
status: http.StatusRequestHeaderFieldsTooLarge,
|
||||||
|
|||||||
+1
-1
@@ -320,7 +320,7 @@ func asTheVisitorSentIt(r *http.Request, body []byte) *http.Request {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// allow answers a question with DispositionAllow and, while
|
// allow answers a question with DispositionAllow and, while
|
||||||
// SWW_WAF_ADD_LOOKUP_HEADERS is true, the client's AS number and country.
|
// SWW_WAF_ADD_LOOKUP_HEADERS is set, the client's AS number and country.
|
||||||
func (rq *request) allow() {
|
func (rq *request) allow() {
|
||||||
rq.allowed = true
|
rq.allowed = true
|
||||||
|
|
||||||
|
|||||||
@@ -869,7 +869,7 @@ func TestAllowCarriesTheLookupHeadersWhileTheSettingIsSet(t *testing.T) {
|
|||||||
wantASN, wantCountry string
|
wantASN, wantCountry string
|
||||||
}{
|
}{
|
||||||
{on, "AS64496", "DE"},
|
{on, "AS64496", "DE"},
|
||||||
{falseValue, "", ""},
|
{"false", "", ""},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.setting, func(t *testing.T) {
|
t.Run(tc.setting, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -1690,12 +1690,7 @@ func TestALargeBodyAnnouncedAndLongHeadersAreNoReasonToRefuse(t *testing.T) {
|
|||||||
strings.NewReader("a body"))
|
strings.NewReader("a body"))
|
||||||
req.Header.Set(waf.ContentLengthHeader, "1073741824")
|
req.Header.Set(waf.ContentLengthHeader, "1073741824")
|
||||||
req.Header.Set(forwardedFor, otherClient)
|
req.Header.Set(forwardedFor, otherClient)
|
||||||
|
req.Header.Set("X-Long", strings.Repeat("x", 40<<10))
|
||||||
// 40,000 bytes of headers, over rproxy's 32 KiB, in lines within the
|
|
||||||
// header limits.
|
|
||||||
for i := range 5 {
|
|
||||||
req.Header.Set("X-Long-"+strconv.Itoa(i), strings.Repeat("x", 8000))
|
|
||||||
}
|
|
||||||
|
|
||||||
got := do(t, req)
|
got := do(t, req)
|
||||||
wantDisposition(t, got, http.StatusOK, waf.DispositionAllow)
|
wantDisposition(t, got, http.StatusOK, waf.DispositionAllow)
|
||||||
|
|||||||
Reference in new issue
Block a user