Skip to content

Commit aebd8a3

Browse files
committed
bench: add CRS XML body processor benchmarks for both XML and xmlquery
Add matching benchmarks for the original XML and experimental xmlquery body processors processing XML request bodies through the full CRS rule pipeline: - BenchmarkCRSXMLSimplePOST / BenchmarkCRSXMLQuerySimplePOST: Small XML-RPC payload (~250B) - BenchmarkCRSXMLLargeSOAP / BenchmarkCRSXMLQueryLargeSOAP: Large SOAP envelope (~15KB, 100 items, namespaces) Results on Apple M2 show xmlquery is ~1.8-2x slower due to full DOM construction, which is the expected tradeoff for real XPath support.
1 parent 868ef5a commit aebd8a3

2 files changed

Lines changed: 245 additions & 0 deletions

File tree

testing/coreruleset/coreruleset_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,101 @@ func BenchmarkCRSLargePOST(b *testing.B) {
151151
}
152152
}
153153

154+
func BenchmarkCRSXMLSimplePOST(b *testing.B) {
155+
waf := crsWAF(b)
156+
157+
xmlPayload := []byte(`<?xml version="1.0"?>
158+
<methodCall>
159+
<methodName>wp.getUsersBlogs</methodName>
160+
<params>
161+
<param><value><string>admin</string></value></param>
162+
<param><value><string>password123</string></value></param>
163+
</params>
164+
</methodCall>`)
165+
166+
b.ReportAllocs()
167+
b.ResetTimer()
168+
for i := 0; i < b.N; i++ {
169+
tx := waf.NewTransaction()
170+
tx.ProcessConnection("127.0.0.1", 8080, "127.0.0.1", 8080)
171+
tx.ProcessURI("/xmlrpc.php", "POST", "HTTP/1.1")
172+
tx.AddRequestHeader("Host", "localhost")
173+
tx.AddRequestHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36")
174+
tx.AddRequestHeader("Content-Type", "text/xml")
175+
tx.ProcessRequestHeaders()
176+
if _, _, err := tx.WriteRequestBody(xmlPayload); err != nil {
177+
b.Error(err)
178+
}
179+
if _, err := tx.ProcessRequestBody(); err != nil {
180+
b.Error(err)
181+
}
182+
tx.AddResponseHeader("Content-Type", "text/xml")
183+
tx.ProcessResponseHeaders(200, "OK")
184+
if _, err := tx.ProcessResponseBody(); err != nil {
185+
b.Error(err)
186+
}
187+
tx.ProcessLogging()
188+
if err := tx.Close(); err != nil {
189+
b.Error(err)
190+
}
191+
}
192+
}
193+
194+
func BenchmarkCRSXMLLargeSOAP(b *testing.B) {
195+
waf := crsWAF(b)
196+
197+
// ~4KB SOAP envelope with multiple items
198+
var sb strings.Builder
199+
sb.WriteString(`<?xml version="1.0" encoding="UTF-8"?>
200+
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
201+
xmlns:ns="http://example.com/api">
202+
<soap:Header>
203+
<ns:Auth token="bearer-abc123"/>
204+
</soap:Header>
205+
<soap:Body>
206+
<ns:BatchRequest>`)
207+
for i := 0; i < 100; i++ {
208+
fmt.Fprintf(&sb, `
209+
<ns:Item id="%d" priority="normal">
210+
<ns:Name>Item number %d</ns:Name>
211+
<ns:Description>Description for item %d with some extra text to add size</ns:Description>
212+
<ns:Value>%d.99</ns:Value>
213+
</ns:Item>`, i, i, i, i*10+99)
214+
}
215+
sb.WriteString(`
216+
</ns:BatchRequest>
217+
</soap:Body>
218+
</soap:Envelope>`)
219+
xmlPayload := []byte(sb.String())
220+
221+
b.ReportAllocs()
222+
b.ResetTimer()
223+
for i := 0; i < b.N; i++ {
224+
tx := waf.NewTransaction()
225+
tx.ProcessConnection("127.0.0.1", 8080, "127.0.0.1", 8080)
226+
tx.ProcessURI("/api/batch", "POST", "HTTP/1.1")
227+
tx.AddRequestHeader("Host", "localhost")
228+
tx.AddRequestHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36")
229+
tx.AddRequestHeader("Content-Type", "application/soap+xml")
230+
tx.ProcessRequestHeaders()
231+
if _, _, err := tx.WriteRequestBody(xmlPayload); err != nil {
232+
b.Error(err)
233+
}
234+
if _, err := tx.ProcessRequestBody(); err != nil {
235+
b.Error(err)
236+
}
237+
tx.AddResponseHeader("Content-Type", "application/soap+xml")
238+
tx.ProcessResponseHeaders(200, "OK")
239+
if _, err := tx.ProcessResponseBody(); err != nil {
240+
b.Error(err)
241+
}
242+
tx.ProcessLogging()
243+
if err := tx.Close(); err != nil {
244+
b.Error(err)
245+
}
246+
}
247+
}
248+
154249
// BenchmarkCRSPrefilter measures CRS request processing across diverse traffic
155250
// patterns. Run with and without the coraza.rule.rx_prefilter build tag and
156251
// compare via benchstat:

testing/coreruleset/coreruleset_xmlquery_test.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package coreruleset
88

99
import (
1010
"bufio"
11+
"fmt"
1112
"io"
1213
"io/fs"
1314
"net/http"
@@ -16,6 +17,7 @@ import (
1617
"os"
1718
"path/filepath"
1819
"strconv"
20+
"strings"
1921
"testing"
2022
"time"
2123

@@ -185,3 +187,151 @@ SecRule REQUEST_HEADERS:X-CRS-Test "@rx ^.*$" \
185187
t.Errorf("[fatal] %d failed tests: %v", totalFailed, res.Stats.Failed)
186188
}
187189
}
190+
191+
// crsWAFXMLQuery returns a CRS WAF configured to use the experimental xmlquery
192+
// body processor for XML content types instead of the default XML processor.
193+
func crsWAFXMLQuery(t testing.TB) coraza.WAF {
194+
t.Helper()
195+
rec, err := os.ReadFile(filepath.Join("..", "..", "coraza.conf-recommended"))
196+
if err != nil {
197+
t.Fatal(err)
198+
}
199+
customTestingConfig := `
200+
SecResponseBodyMimeType text/plain
201+
SecDefaultAction "phase:3,log,auditlog,pass"
202+
SecDefaultAction "phase:4,log,auditlog,pass"
203+
204+
# Rule 900005 from https://github.com/coreruleset/coreruleset/blob/v4.0/dev/tests/regression/README.md#requirements
205+
SecAction "id:900005,\
206+
phase:1,\
207+
nolog,\
208+
pass,\
209+
ctl:ruleEngine=DetectionOnly,\
210+
ctl:ruleRemoveById=910000,\
211+
setvar:tx.blocking_paranoia_level=4,\
212+
setvar:tx.crs_validate_utf8_encoding=1,\
213+
setvar:tx.arg_name_length=100,\
214+
setvar:tx.arg_length=400,\
215+
setvar:tx.total_arg_length=64000,\
216+
setvar:tx.max_num_args=255,\
217+
setvar:tx.max_file_size=64100,\
218+
setvar:tx.combined_file_sizes=65535"
219+
`
220+
xmlqueryOverride := `
221+
SecRuleUpdateActionById 200000 "phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XMLQUERY"
222+
`
223+
conf := coraza.NewWAFConfig().
224+
WithRootFS(coreruleset.FS).
225+
WithDirectives(string(rec)).
226+
WithDirectives(customTestingConfig).
227+
WithDirectives("Include @crs-setup.conf.example").
228+
WithDirectives("Include @owasp_crs/*.conf").
229+
WithDirectives(xmlqueryOverride)
230+
231+
waf, err := coraza.NewWAF(conf)
232+
if err != nil {
233+
t.Fatal(err)
234+
}
235+
if closer, ok := waf.(experimental.WAFCloser); ok {
236+
if _, isBenchmark := t.(*testing.B); !isBenchmark {
237+
t.Cleanup(func() { closer.Close() })
238+
}
239+
}
240+
241+
return waf
242+
}
243+
244+
func BenchmarkCRSXMLQuerySimplePOST(b *testing.B) {
245+
waf := crsWAFXMLQuery(b)
246+
247+
xmlPayload := []byte(`<?xml version="1.0"?>
248+
<methodCall>
249+
<methodName>wp.getUsersBlogs</methodName>
250+
<params>
251+
<param><value><string>admin</string></value></param>
252+
<param><value><string>password123</string></value></param>
253+
</params>
254+
</methodCall>`)
255+
256+
b.ReportAllocs()
257+
b.ResetTimer()
258+
for i := 0; i < b.N; i++ {
259+
tx := waf.NewTransaction()
260+
tx.ProcessConnection("127.0.0.1", 8080, "127.0.0.1", 8080)
261+
tx.ProcessURI("/xmlrpc.php", "POST", "HTTP/1.1")
262+
tx.AddRequestHeader("Host", "localhost")
263+
tx.AddRequestHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36")
264+
tx.AddRequestHeader("Content-Type", "text/xml")
265+
tx.ProcessRequestHeaders()
266+
if _, _, err := tx.WriteRequestBody(xmlPayload); err != nil {
267+
b.Error(err)
268+
}
269+
if _, err := tx.ProcessRequestBody(); err != nil {
270+
b.Error(err)
271+
}
272+
tx.AddResponseHeader("Content-Type", "text/xml")
273+
tx.ProcessResponseHeaders(200, "OK")
274+
if _, err := tx.ProcessResponseBody(); err != nil {
275+
b.Error(err)
276+
}
277+
tx.ProcessLogging()
278+
if err := tx.Close(); err != nil {
279+
b.Error(err)
280+
}
281+
}
282+
}
283+
284+
func BenchmarkCRSXMLQueryLargeSOAP(b *testing.B) {
285+
waf := crsWAFXMLQuery(b)
286+
287+
// ~4KB SOAP envelope with multiple items
288+
var sb strings.Builder
289+
sb.WriteString(`<?xml version="1.0" encoding="UTF-8"?>
290+
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
291+
xmlns:ns="http://example.com/api">
292+
<soap:Header>
293+
<ns:Auth token="bearer-abc123"/>
294+
</soap:Header>
295+
<soap:Body>
296+
<ns:BatchRequest>`)
297+
for i := 0; i < 100; i++ {
298+
fmt.Fprintf(&sb, `
299+
<ns:Item id="%d" priority="normal">
300+
<ns:Name>Item number %d</ns:Name>
301+
<ns:Description>Description for item %d with some extra text to add size</ns:Description>
302+
<ns:Value>%d.99</ns:Value>
303+
</ns:Item>`, i, i, i, i*10+99)
304+
}
305+
sb.WriteString(`
306+
</ns:BatchRequest>
307+
</soap:Body>
308+
</soap:Envelope>`)
309+
xmlPayload := []byte(sb.String())
310+
311+
b.ReportAllocs()
312+
b.ResetTimer()
313+
for i := 0; i < b.N; i++ {
314+
tx := waf.NewTransaction()
315+
tx.ProcessConnection("127.0.0.1", 8080, "127.0.0.1", 8080)
316+
tx.ProcessURI("/api/batch", "POST", "HTTP/1.1")
317+
tx.AddRequestHeader("Host", "localhost")
318+
tx.AddRequestHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36")
319+
tx.AddRequestHeader("Content-Type", "application/soap+xml")
320+
tx.ProcessRequestHeaders()
321+
if _, _, err := tx.WriteRequestBody(xmlPayload); err != nil {
322+
b.Error(err)
323+
}
324+
if _, err := tx.ProcessRequestBody(); err != nil {
325+
b.Error(err)
326+
}
327+
tx.AddResponseHeader("Content-Type", "application/soap+xml")
328+
tx.ProcessResponseHeaders(200, "OK")
329+
if _, err := tx.ProcessResponseBody(); err != nil {
330+
b.Error(err)
331+
}
332+
tx.ProcessLogging()
333+
if err := tx.Close(); err != nil {
334+
b.Error(err)
335+
}
336+
}
337+
}

0 commit comments

Comments
 (0)