Compare commits

..
1 Commits
Author SHA1 Message Date
clawbot fbe87a7c5e Core Rule Set reads request bodies up to SWWAF_WAF_BODY_LIMIT (closes #116)
check / check (push) Waiting to run
SWWAF_WAF_BODY_LIMIT (default off, at most 1G) has the Core Rule Set read
form data and multipart up to the limit, the rest streaming on, and JSON
and XML no larger than it, with text/json and the application and text
types ending in +json or +xml. The part read is held for the app. A size
or time limit met while reading ends the request. Content-Encoding is
refused again on these kinds. A body Coraza cannot parse, or a multipart
body failing its strict checks, adds 5, as does a multipart body the limit
cuts before the colon of a part's header. Coraza is built with
no_fs_access, so writes no file. Rule 900300 moves to phase 2.

Judgement call: Content-Encoding is refused on a JSON or XML body too
large to read, as SPEC.md allows.

Model: opus-5-5
2026-10-08 10:04:58 +00:00
3 changed files with 47 additions and 39 deletions
+21 -19
View File
@@ -714,25 +714,27 @@ effective settings are logged at start, unless `SWWAF_LOG_LEVEL` is `warn` or
request body. A size, such as `128K`, at most `1G`, has it read a body of form
data or multipart up to that size, the rest of a longer one passing on to the
app as it arrives, without being held, and a JSON or XML body no larger than
that size, since those cannot be read in part. A body whose type ends in
`+json`, or is `text/json`, is JSON, and one whose type ends in `+xml` is XML.
Any other body reaches the app uninspected, and so does a larger JSON or XML
body: the Core Rule Set would read any other body as form data, where binary
content such as a git push trips rules written for text. A body it reads that
Coraza cannot parse (rule 900440), and a multipart body that fails Coraza's
strict checks (rule 900450), add 5 to the score, as a rule rated critical
does, since no rule reads what comes after the fault; a multipart body that
reaches the limit does not, since the limit can cut it inside a part's header.
The client has until `SWWAF_CLIENT_REQUEST_TIMEOUT` runs out to send the part
that is read, and a request whose body passes `SWWAF_REQUEST_MAX_BYTES` within
it is refused before anything reaches the app. Of a file in a multipart body,
Coraza counts the bytes and writes nothing. Body inspection suits apps whose
forms carry no code. In front of gitea it refuses 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), and attachments named like `debug.log` or `config.yml`
(932180), until the rule ids the request log names are added to
`SWWAF_WAF_DISABLED_RULES`.
that size, since those cannot be read in part. A body is JSON when its type is
`application/json` or `text/json`, or an `application/` or `text/` type ending
in `+json`, and XML when its type is `application/xml` or `text/xml`, or an
`application/` or `text/` type ending in `+xml`. Any other body reaches the
app uninspected, and so does a larger JSON or XML body: the Core Rule Set
would read any other body as form data, where binary content such as a git
push trips rules written for text. A body it reads that Coraza cannot parse
(rule 900440), and a multipart body that fails Coraza's strict checks (rule
900450), add 5 to the score, as a rule rated critical does, since no rule
reads what comes after the fault. So does a multipart body the limit cuts
before the colon of a part's header line, which Coraza cannot tell from a
header without one. The client has until `SWWAF_CLIENT_REQUEST_TIMEOUT` runs
out to send the part that is read, and a request whose body passes
`SWWAF_REQUEST_MAX_BYTES` within it is refused before anything reaches the
app. Of a file in a multipart body, Coraza counts the bytes and writes
nothing. Body inspection suits apps whose forms carry no code. In front of
gitea it refuses 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), and attachments
named like `debug.log` or `config.yml` (932180), until the rule ids the
request log names are added to `SWWAF_WAF_DISABLED_RULES`.
- `SWWAF_TRAP_PATHS` (default empty): paths the app never serves and only
scanners ask for, such as `/wp-login.php,/xmlrpc.php` in front of gitea; a
request for one bans its client for a clear sign of attack, as a `ban` rule
+7 -10
View File
@@ -105,9 +105,10 @@ SecRuleUpdateTargetById 944110 "!REQUEST_HEADERS:Referer"
// bodyDirectives have the Core Rule Set read the part of a request body
// Inspect gives it, which is at most one byte longer than the limit, up to
// the limit, %d bytes, and read JSON and XML as Coraza's recommended
// configuration has it in its rules 200000, 200001 and 200006, with any
// type ending in +xml, and text/json, besides; form data and multipart
// Coraza knows by itself. %% stands for a % Coraza reads.
// configuration has it in its rules 200000, 200001 and 200006, with
// text/json, and any application or text type ending in +xml or +json,
// besides; form data and multipart Coraza knows by itself. %% stands for
// a % Coraza reads.
const bodyDirectives = `
SecRequestBodyAccess On
SecRequestBodyLimit %d
@@ -132,13 +133,9 @@ SecRule REQBODY_PROCESSOR "@rx ^(?:URLENCODED|MULTIPART|JSON|XML)$" \
# its strict checks (900450), each add 5 to the score, as a rule the Core
# Rule Set rates critical does: no rule reads what comes after the fault,
# which the app may still read. Coraza's recommended configuration refuses
# them in its rules 200002 and 200003. A multipart body that reaches the
# limit is let off both (900430), since the limit can cut it inside a
# part's header, which Coraza takes for such a fault. Coraza parses any
# form data body.
SecRule INBOUND_DATA_ERROR "@eq 1" "id:900430,phase:2,pass,nolog,chain"
SecRule REQBODY_PROCESSOR "@streq MULTIPART" \
"ctl:ruleRemoveById=900440,ctl:ruleRemoveById=900450"
# them in its rules 200002 and 200003. A multipart body the limit cuts
# before the colon of a part's header line adds 5 too, since Coraza cannot
# tell it from a header without one. Coraza parses any form data body.
SecRule REQBODY_ERROR "!@eq 0" "id:900440,phase:2,pass,severity:'CRITICAL',\
setvar:'tx.inbound_anomaly_score_pl1=+5'"
SecRule MULTIPART_STRICT_ERROR "!@eq 0" "id:900450,phase:2,pass,\
+19 -10
View File
@@ -613,26 +613,35 @@ func TestBodyCorazaCannotParseIsAMatch(t *testing.T) {
// The multipart bodies Coraza cannot parse fail its strict checks too:
// one whose type names its boundary twice, and one with a part header
// that has no colon, before the attack.
body = field("q", injection) + end
wantBody(t, crs, post(multipart+"; boundary=c"), body, matched(900440, 900450),
body)
// that has no colon, before the attack. They do so padded past the
// limit too, which cuts them in the padding.
noColon := "--b\r\nContent-Disposition form-data; name=\"a\"\r\n\r\n1\r\n"
pad := field("pad", strings.Repeat("a", bodyLimit))
body = "--b\r\nContent-Disposition form-data; name=\"a\"\r\n\r\n1\r\n" +
field("q", injection) + end
wantBody(t, crs, post(multipart), body, matched(900440, 900450), body)
for _, tc := range []struct{ header, head string }{
{multipart + "; boundary=c", ""},
{multipart, noColon},
} {
body = tc.head + field("q", injection) + end
wantBody(t, crs, post(tc.header), body, matched(900440, 900450), body)
body = tc.head + field("q", injection) + pad + end
wantBody(t, crs, post(tc.header), body, matched(900440, 900450),
body[:bodyLimit+1])
}
}
func TestMultipartBodyCutInsideAPartHeaderIsNotAMatch(t *testing.T) {
func TestMultipartBodyCutBeforeAPartHeadersColonIsAMatch(t *testing.T) {
t.Parallel()
// The limit falls in the middle of the name of the second part's
// header, which Coraza, reading up to the limit, finds without a colon.
// header, which Coraza, reading up to the limit, cannot tell from a
// header without a colon.
cut := "--b\r\nContent-Di"
first := field("pad", strings.Repeat("a", bodyLimit-len(field("pad", ""))-len(cut)))
body := first + cut + "sposition: form-data; name=\"q\"\r\n\r\n1\r\n" + end
wantBody(t, readingBodies(t), post(multipart), body, waf.Result{},
wantBody(t, readingBodies(t), post(multipart), body, matched(900440, 900450),
body[:bodyLimit+1])
}