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 29 additions and 60 deletions
+9 -13
View File
@@ -714,19 +714,15 @@ 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
before the colon of a part's header line, or between the carriage return and
the line feed that end a part's header line or the empty line after its
headers, since Coraza takes the line the limit cuts for a malformed header.
The client has until `SWWAF_CLIENT_REQUEST_TIMEOUT` runs out to send the part 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 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, it is refused before anything reaches the app. Of a file in a multipart body,
+10 -9
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,11 +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, or between the carriage return # limit is let off both (900430), since the limit can cut it inside a
# and the line feed that end a part's header line or the empty line after # part's header, which Coraza takes for such a fault. Coraza parses any
# its headers, adds 5 too, since Coraza takes the line the limit cuts for a # form data body.
# malformed header. 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 -38
View File
@@ -613,57 +613,29 @@ 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])
} }
func TestMultipartBodyCutBeforeALineFeedInAPartsHeadersIsAMatch(t *testing.T) {
t.Parallel()
crs := readingBodies(t)
headerLine := "--b\r\nContent-Disposition: form-data; name=\"q\"\r"
// The limit falls between the carriage return and the line feed that
// end the second part's header line, and then between those that end
// the empty line after it. Coraza, reading up to the limit, takes the
// line ending in a lone carriage return for a malformed header.
for _, cut := range []int{len(headerLine), len(headerLine + "\n\r")} {
first := field("pad", strings.Repeat("a", bodyLimit-len(field("pad", ""))-cut))
body := first + field("q", "1") + end
wantBody(t, crs, post(multipart), body, matched(900440, 900450),
body[:bodyLimit+1])
}
}
// TestCorazaWritesNoFile is not parallel, since it sets TMPDIR, the // TestCorazaWritesNoFile is not parallel, since it sets TMPDIR, the
// system's temporary directory, for the whole test process. // system's temporary directory, for the whole test process.
func TestCorazaWritesNoFile(t *testing.T) { func TestCorazaWritesNoFile(t *testing.T) {