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 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 whose type ends in that size, since those cannot be read in part. A body is JSON when its type is
`+json`, or is `text/json`, is JSON, and one whose type ends in `+xml` is XML. `application/json` or `text/json`, or an `application/` or `text/` type ending
Any other body reaches the app uninspected, and so does a larger JSON or XML in `+json`, and XML when its type is `application/xml` or `text/xml`, or an
body: the Core Rule Set would read any other body as form data, where binary `application/` or `text/` type ending in `+xml`. Any other body reaches the
content such as a git push trips rules written for text. A body it reads that app uninspected, and so does a larger JSON or XML body: the Core Rule Set
Coraza cannot parse (rule 900440), and a multipart body that fails Coraza's would read any other body as form data, where binary content such as a git
strict checks (rule 900450), add 5 to the score, as a rule rated critical push trips rules written for text. A body it reads that Coraza cannot parse
does, since no rule reads what comes after the fault; a multipart body that (rule 900440), and a multipart body that fails Coraza's strict checks (rule
reaches the limit does not, since the limit can cut it inside a part's header. 900450), add 5 to the score, as a rule rated critical does, since no rule
The client has until `SWWAF_CLIENT_REQUEST_TIMEOUT` runs out to send the part reads what comes after the fault. So does a multipart body the limit cuts
that is read, and a request whose body passes `SWWAF_REQUEST_MAX_BYTES` within before the colon of a part's header line, which Coraza cannot tell from a
it is refused before anything reaches the app. Of a file in a multipart body, header without one. The client has until `SWWAF_CLIENT_REQUEST_TIMEOUT` runs
Coraza counts the bytes and writes nothing. Body inspection suits apps whose out to send the part that is read, and a request whose body passes
forms carry no code. In front of gitea it refuses issue and comment text, wiki `SWWAF_REQUEST_MAX_BYTES` within it is refused before anything reaches the
pages and files saved in the web editor that hold shell commands or code app. Of a file in a multipart body, Coraza counts the bytes and writes
(932125, 932235, 932250 and others), package descriptions that show code, PyPI nothing. Body inspection suits apps whose forms carry no code. In front of
uploads (922130), and attachments named like `debug.log` or `config.yml` gitea it refuses issue and comment text, wiki pages and files saved in the web
(932180), until the rule ids the request log names are added to editor that hold shell commands or code (932125, 932235, 932250 and others),
`SWWAF_WAF_DISABLED_RULES`. 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 - `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
+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 // 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 any // configuration has it in its rules 200000, 200001 and 200006, with
// type ending in +xml, and text/json, besides; form data and multipart // text/json, and any application or text type ending in +xml or +json,
// Coraza knows by itself. %% stands for a % Coraza reads. // besides; form data and multipart Coraza knows by itself. %% stands for
// a % Coraza reads.
const bodyDirectives = ` const bodyDirectives = `
SecRequestBodyAccess On SecRequestBodyAccess On
SecRequestBodyLimit %d 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 # 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 that reaches the # them in its rules 200002 and 200003. A multipart body the limit cuts
# limit is let off both (900430), since the limit can cut it inside a # before the colon of a part's header line adds 5 too, since Coraza cannot
# part's header, which Coraza takes for such a fault. Coraza parses any # tell it from a header without one. Coraza parses any form data body.
# 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,\
+19 -10
View File
@@ -613,26 +613,35 @@ 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. // that has no colon, before the attack. They do so padded past the
body = field("q", injection) + end // limit too, which cuts them in the padding.
wantBody(t, crs, post(multipart+"; boundary=c"), body, matched(900440, 900450), noColon := "--b\r\nContent-Disposition form-data; name=\"a\"\r\n\r\n1\r\n"
body) pad := field("pad", strings.Repeat("a", bodyLimit))
body = "--b\r\nContent-Disposition form-data; name=\"a\"\r\n\r\n1\r\n" + for _, tc := range []struct{ header, head string }{
field("q", injection) + end {multipart + "; boundary=c", ""},
wantBody(t, crs, post(multipart), body, matched(900440, 900450), body) {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() 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, 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" 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, waf.Result{}, wantBody(t, readingBodies(t), post(multipart), body, matched(900440, 900450),
body[:bodyLimit+1]) body[:bodyLimit+1])
} }