Compare commits

..
1 Commits
Author SHA1 Message Date
clawbot 0fb0675588 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 (with +json, text/json and +xml) no larger than it. 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, but
not a multipart body reaching the limit. 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 09:20:49 +00:00
3 changed files with 39 additions and 47 deletions
+19 -21
View File
@@ -714,27 +714,25 @@ 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 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 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 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 is JSON when its type is that size, since those cannot be read in part. A body whose type ends in
`application/json` or `text/json`, or an `application/` or `text/` type ending `+json`, or is `text/json`, is JSON, and one whose type ends in `+xml` is XML.
in `+json`, and XML when its type is `application/xml` or `text/xml`, or an Any other body reaches the app uninspected, and so does a larger JSON or XML
`application/` or `text/` type ending in `+xml`. Any other body reaches the body: the Core Rule Set would read any other body as form data, where binary
app uninspected, and so does a larger JSON or XML body: the Core Rule Set content such as a git push trips rules written for text. A body it reads that
would read any other body as form data, where binary content such as a git Coraza cannot parse (rule 900440), and a multipart body that fails Coraza's
push trips rules written for text. A body it reads that Coraza cannot parse strict checks (rule 900450), add 5 to the score, as a rule rated critical
(rule 900440), and a multipart body that fails Coraza's strict checks (rule does, since no rule reads what comes after the fault; a multipart body that
900450), add 5 to the score, as a rule rated critical does, since no rule reaches the limit does not, since the limit can cut it inside a part's header.
reads what comes after the fault. So does a multipart body the limit cuts The client has until `SWWAF_CLIENT_REQUEST_TIMEOUT` runs out to send the part
before the colon of a part's header line, which Coraza cannot tell from a that is read, and a request whose body passes `SWWAF_REQUEST_MAX_BYTES` within
header without one. The client has until `SWWAF_CLIENT_REQUEST_TIMEOUT` runs it is refused before anything reaches the app. Of a file in a multipart body,
out to send the part that is read, and a request whose body passes Coraza counts the bytes and writes nothing. Body inspection suits apps whose
`SWWAF_REQUEST_MAX_BYTES` within it is refused before anything reaches the forms carry no code. In front of gitea it refuses issue and comment text, wiki
app. Of a file in a multipart body, Coraza counts the bytes and writes pages and files saved in the web editor that hold shell commands or code
nothing. Body inspection suits apps whose forms carry no code. In front of (932125, 932235, 932250 and others), package descriptions that show code, PyPI
gitea it refuses issue and comment text, wiki pages and files saved in the web uploads (922130), and attachments named like `debug.log` or `config.yml`
editor that hold shell commands or code (932125, 932235, 932250 and others), (932180), until the rule ids the request log names are added to
package descriptions that show code, PyPI uploads (922130), and attachments `SWWAF_WAF_DISABLED_RULES`.
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 - `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 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 request for one bans its client for a clear sign of attack, as a `ban` rule
+10 -7
View File
@@ -105,10 +105,9 @@ SecRuleUpdateTargetById 944110 "!REQUEST_HEADERS:Referer"
// bodyDirectives have the Core Rule Set read the part of a request body // 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 // 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 // the limit, %d bytes, and read JSON and XML as Coraza's recommended
// configuration has it in its rules 200000, 200001 and 200006, with // configuration has it in its rules 200000, 200001 and 200006, with any
// text/json, and any application or text type ending in +xml or +json, // type ending in +xml, and text/json, besides; form data and multipart
// besides; form data and multipart Coraza knows by itself. %% stands for // Coraza knows by itself. %% stands for a % Coraza reads.
// a % Coraza reads.
const bodyDirectives = ` const bodyDirectives = `
SecRequestBodyAccess On SecRequestBodyAccess On
SecRequestBodyLimit %d SecRequestBodyLimit %d
@@ -133,9 +132,13 @@ SecRule REQBODY_PROCESSOR "@rx ^(?:URLENCODED|MULTIPART|JSON|XML)$" \
# its strict checks (900450), each add 5 to the score, as a rule the Core # 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, # Rule Set rates critical does: no rule reads what comes after the fault,
# which the app may still read. Coraza's recommended configuration refuses # which the app may still read. Coraza's recommended configuration refuses
# them in its rules 200002 and 200003. A multipart body the limit cuts # them in its rules 200002 and 200003. A multipart body that reaches the
# before the colon of a part's header line adds 5 too, since Coraza cannot # limit is let off both (900430), since the limit can cut it inside a
# tell it from a header without one. Coraza parses any form data body. # 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"
SecRule REQBODY_ERROR "!@eq 0" "id:900440,phase:2,pass,severity:'CRITICAL',\ SecRule REQBODY_ERROR "!@eq 0" "id:900440,phase:2,pass,severity:'CRITICAL',\
setvar:'tx.inbound_anomaly_score_pl1=+5'" setvar:'tx.inbound_anomaly_score_pl1=+5'"
SecRule MULTIPART_STRICT_ERROR "!@eq 0" "id:900450,phase:2,pass,\ SecRule MULTIPART_STRICT_ERROR "!@eq 0" "id:900450,phase:2,pass,\
+10 -19
View File
@@ -613,35 +613,26 @@ func TestBodyCorazaCannotParseIsAMatch(t *testing.T) {
// The multipart bodies Coraza cannot parse fail its strict checks too: // The multipart bodies Coraza cannot parse fail its strict checks too:
// one whose type names its boundary twice, and one with a part header // one whose type names its boundary twice, and one with a part header
// that has no colon, before the attack. They do so padded past the // that has no colon, before the attack.
// limit too, which cuts them in the padding. body = field("q", injection) + end
noColon := "--b\r\nContent-Disposition form-data; name=\"a\"\r\n\r\n1\r\n" wantBody(t, crs, post(multipart+"; boundary=c"), body, matched(900440, 900450),
pad := field("pad", strings.Repeat("a", bodyLimit)) body)
for _, tc := range []struct{ header, head string }{ body = "--b\r\nContent-Disposition form-data; name=\"a\"\r\n\r\n1\r\n" +
{multipart + "; boundary=c", ""}, field("q", injection) + end
{multipart, noColon}, wantBody(t, crs, post(multipart), body, matched(900440, 900450), body)
} {
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 TestMultipartBodyCutBeforeAPartHeadersColonIsAMatch(t *testing.T) { func TestMultipartBodyCutInsideAPartHeaderIsNotAMatch(t *testing.T) {
t.Parallel() t.Parallel()
// The limit falls in the middle of the name of the second part's // The limit falls in the middle of the name of the second part's
// header, which Coraza, reading up to the limit, cannot tell from a // header, which Coraza, reading up to the limit, finds without a colon.
// header without a colon.
cut := "--b\r\nContent-Di" cut := "--b\r\nContent-Di"
first := field("pad", strings.Repeat("a", bodyLimit-len(field("pad", ""))-len(cut))) 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 body := first + cut + "sposition: form-data; name=\"q\"\r\n\r\n1\r\n" + end
wantBody(t, readingBodies(t), post(multipart), body, matched(900440, 900450), wantBody(t, readingBodies(t), post(multipart), body, waf.Result{},
body[:bodyLimit+1]) body[:bodyLimit+1])
} }