Compare commits

..
1 Commits
Author SHA1 Message Date
clawbot 6fcbda6ece 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 in a part's headers before a colon or a line feed. 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 12:59:13 +02:00
3 changed files with 60 additions and 29 deletions
+13 -9
View File
@@ -714,15 +714,19 @@ 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
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,
+9 -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,11 @@ 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, or between the carriage return
# part's header, which Coraza takes for such a fault. Coraza parses any # and the line feed that end a part's header line or the empty line after
# form data body. # its headers, adds 5 too, since Coraza takes the line the limit cuts for a
SecRule INBOUND_DATA_ERROR "@eq 1" "id:900430,phase:2,pass,nolog,chain" # malformed header. Coraza parses any form data body.
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,\
+38 -10
View File
@@ -613,29 +613,57 @@ 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])
} }
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) {