Skip to content

Commit 393d4d6

Browse files
authored
fix: chunk bodies and process partial (#252)
1 parent 475eda6 commit 393d4d6

File tree

5 files changed

+121
-69
lines changed

5 files changed

+121
-69
lines changed

.github/workflows/ci.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ env:
1616
TINYGO_VERSION: 0.30.0
1717
# Run e2e tests against latest two releases and latest dev
1818
ENVOY_IMAGES: >
19+
envoyproxy/envoy:v1.28-latest
1920
envoyproxy/envoy:v1.27-latest
20-
envoyproxy/envoy:v1.26-latest
2121
envoyproxy/envoy-dev:latest
22-
istio/proxyv2:1.18.2
23-
istio/proxyv2:1.19.0
22+
istio/proxyv2:1.20.1
23+
istio/proxyv2:1.19.5
2424
2525
jobs:
2626
build:

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ go 1.20
55
require (
66
github.com/corazawaf/coraza-wasilibs v0.1.0
77
github.com/corazawaf/coraza/v3 v3.0.4
8-
github.com/stretchr/testify v1.8.0
9-
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0
8+
github.com/stretchr/testify v1.8.4
9+
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.1-0.20240102162926-b089ccb94219
1010
github.com/tidwall/gjson v1.17.0
1111
github.com/wasilibs/nottinygc v0.7.0
1212
)
@@ -18,7 +18,7 @@ require (
1818
github.com/magefile/mage v1.15.0 // indirect
1919
github.com/petar-dambovaliev/aho-corasick v0.0.0-20230725210150-fb29fc3c913e // indirect
2020
github.com/pmezard/go-difflib v1.0.0 // indirect
21-
github.com/tetratelabs/wazero v1.5.0 // indirect
21+
github.com/tetratelabs/wazero v1.6.0 // indirect
2222
github.com/tidwall/match v1.1.1 // indirect
2323
github.com/tidwall/pretty v1.2.1 // indirect
2424
github.com/wasilibs/go-aho-corasick v0.5.0 // indirect

go.sum

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ github.com/corazawaf/coraza/v3 v3.0.4 h1:Llemgoh0hp2NggCwcWN8lNiV4Pfe+AWzf1oEcas
44
github.com/corazawaf/coraza/v3 v3.0.4/go.mod h1:3fTYjY5BZv3nezLpH6NAap0gr3jZfbQWUAu2GF17ET4=
55
github.com/corazawaf/libinjection-go v0.1.2 h1:oeiV9pc5rvJ+2oqOqXEAMJousPpGiup6f7Y3nZj5GoM=
66
github.com/corazawaf/libinjection-go v0.1.2/go.mod h1:OP4TM7xdJ2skyXqNX1AN1wN5nNZEmJNuWbNPOItn7aw=
7-
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
87
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
98
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
109
github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6FI=
@@ -20,15 +19,12 @@ github.com/petar-dambovaliev/aho-corasick v0.0.0-20230725210150-fb29fc3c913e h1:
2019
github.com/petar-dambovaliev/aho-corasick v0.0.0-20230725210150-fb29fc3c913e/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw=
2120
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
2221
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
23-
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
24-
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
25-
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
26-
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
27-
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
28-
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0 h1:kS7BvMKN+FiptV4pfwiNX8e3q14evxAWkhYbxt8EI1M=
29-
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0/go.mod h1:qkW5MBz2jch2u8bS59wws65WC+Gtx3x0aPUX5JL7CXI=
30-
github.com/tetratelabs/wazero v1.5.0 h1:Yz3fZHivfDiZFUXnWMPUoiW7s8tC1sjdBtlJn08qYa0=
31-
github.com/tetratelabs/wazero v1.5.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A=
22+
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
23+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
24+
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.1-0.20240102162926-b089ccb94219 h1:lBbCzjjZJl2+deUwrRv5+GQle18AkCoUkFwL8sPVvIE=
25+
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.1-0.20240102162926-b089ccb94219/go.mod h1:YqR8JZaY3Ev9ihXgjzAQAMkXEzPKKmy4Q5rsVWt4XGk=
26+
github.com/tetratelabs/wazero v1.6.0 h1:z0H1iikCdP8t+q341xqepY4EWvHEw8Es7tlqiVzlP3g=
27+
github.com/tetratelabs/wazero v1.6.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A=
3228
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
3329
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
3430
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
@@ -54,7 +50,6 @@ golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
5450
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
5551
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
5652
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
57-
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
5853
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
5954
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
6055
rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=

main_test.go

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"fmt"
1010
"os"
1111
"path/filepath"
12+
"regexp"
13+
"strconv"
1214
"strings"
1315
"testing"
1416

@@ -229,8 +231,8 @@ func TestLifecycle(t *testing.T) {
229231
{
230232
name: "request body accepted, no request body access",
231233
inlineRules: `
232-
SecRuleEngine On\nSecRequestBodyAccess Off\nSecRule REQUEST_BODY \"animal=bear\" \"id:101,phase:2,t:lowercase,deny\"
233-
`,
234+
SecRuleEngine On\nSecRequestBodyAccess Off\nSecRule REQUEST_BODY \"animal=bear\" \"id:101,phase:2,t:lowercase,deny\"
235+
`,
234236
requestHdrsAction: types.ActionContinue,
235237
requestBodyAction: types.ActionContinue,
236238
responseHdrsAction: types.ActionContinue,
@@ -240,8 +242,8 @@ func TestLifecycle(t *testing.T) {
240242
{
241243
name: "request body accepted, payload above process partial",
242244
inlineRules: `
243-
SecRuleEngine On\nSecRequestBodyAccess On\nSecRequestBodyLimit 2\nSecRequestBodyLimitAction ProcessPartial\nSecRule REQUEST_BODY \"animal=bear\" \"id:101,phase:2,t:lowercase,deny\"
244-
`,
245+
SecRuleEngine On\nSecRequestBodyAccess On\nSecRequestBodyLimit 2\nSecRequestBodyLimitAction ProcessPartial\nSecRule REQUEST_BODY \"animal=bear\" \"id:101,phase:2,t:lowercase,deny\"
246+
`,
245247
requestHdrsAction: types.ActionContinue,
246248
requestBodyAction: types.ActionContinue,
247249
responseHdrsAction: types.ActionContinue,
@@ -450,6 +452,16 @@ func TestLifecycle(t *testing.T) {
450452
// Stream bodies in chunks of 5
451453

452454
if requestHdrsAction == types.ActionContinue {
455+
totalBodysent := 0
456+
requestBodyAccess := strings.Contains(tt.inlineRules, "SecRequestBodyAccess On")
457+
requestBodyProcessPartial := strings.Contains(tt.inlineRules, "SecRequestBodyLimitAction ProcessPartial")
458+
var requestBodyLimit int
459+
matches := regexp.MustCompile(`SecRequestBodyLimit (\d+)`).FindStringSubmatch(tt.inlineRules)
460+
if len(matches) > 1 {
461+
var err error
462+
requestBodyLimit, err = strconv.Atoi(matches[1])
463+
require.NoError(t, err)
464+
}
453465
for i := 0; i < len(reqBody); i += 5 {
454466
eos := i+5 >= len(reqBody)
455467
var body []byte
@@ -458,13 +470,20 @@ func TestLifecycle(t *testing.T) {
458470
} else {
459471
body = reqBody[i : i+5]
460472
}
473+
totalBodysent += len(body)
461474
requestBodyAction = host.CallOnRequestBody(id, body, eos)
462-
requestBodyAccess := strings.Contains(tt.inlineRules, "SecRequestBodyAccess On")
463475
switch {
464476
case eos:
465477
requireEqualAction(t, tt.requestBodyAction, requestBodyAction, "unexpected body action, want %q, have %q on end of stream")
466-
case requestBodyAccess:
478+
// Reject: We expect pause in all cases with action Reject: being the limit reached or not
479+
case requestBodyAccess && !requestBodyProcessPartial:
480+
requireEqualAction(t, types.ActionPause, requestBodyAction, "unexpected request body action, want %q, have %q")
481+
// ProcessPartial: we expect pause until the limit is reached
482+
case requestBodyAccess && requestBodyProcessPartial && totalBodysent < requestBodyLimit:
467483
requireEqualAction(t, types.ActionPause, requestBodyAction, "unexpected request body action, want %q, have %q")
484+
// ProcessPartial: we expect tt.requestBodyAction when the limit is reached
485+
case requestBodyAccess && requestBodyProcessPartial && totalBodysent >= requestBodyLimit:
486+
requireEqualAction(t, tt.requestBodyAction, requestBodyAction, "unexpected request body action, want %q, have %q")
468487
default:
469488
requireEqualAction(t, types.ActionContinue, requestBodyAction, "unexpected request body action, want %q, have %q")
470489
}
@@ -478,6 +497,15 @@ func TestLifecycle(t *testing.T) {
478497

479498
if responseHdrsAction == types.ActionContinue {
480499
responseBodyAccess := strings.Contains(tt.inlineRules, "SecResponseBodyAccess On")
500+
responseBodyProcessPartial := strings.Contains(tt.inlineRules, "SecResponseBodyLimitAction ProcessPartial")
501+
var responseBodyLimit int
502+
matches := regexp.MustCompile(`SecResponseBodyLimit (\d+)`).FindStringSubmatch(tt.inlineRules)
503+
if len(matches) > 1 {
504+
var err error
505+
responseBodyLimit, err = strconv.Atoi(matches[1])
506+
require.NoError(t, err)
507+
}
508+
totalBodysent := 0
481509
for i := 0; i < len(respBody); i += 5 {
482510
eos := i+5 >= len(respBody)
483511
var body []byte
@@ -486,15 +514,21 @@ func TestLifecycle(t *testing.T) {
486514
} else {
487515
body = respBody[i : i+5]
488516
}
517+
totalBodysent += len(body)
489518
responseBodyAction := host.CallOnResponseBody(id, body, eos)
490519
switch {
491520
// expectResponseRejectLimitActionSinceFirstChunk: writing the first chunk (len(respBody) bytes), it is expected to reach
492521
// the ResponseBodyLimit with the Action set to Reject. When these conditions happen, ActionContinue will be returned,
493522
// with the interruption enforced replacing the body with null bytes (checked with tt.respondedNullBody)
494523
case eos, tt.expectResponseRejectSinceFirstChunk:
495524
requireEqualAction(t, types.ActionContinue, responseBodyAction, "unexpected response body action, want %q, have %q on end of stream")
496-
case responseBodyAccess:
497-
requireEqualAction(t, types.ActionPause, responseBodyAction, "unexpected response body action, want %q, have %q")
525+
// Reject: We expect pause in all cases with action Reject: being the limit reached or not
526+
// It would either be paused because we are callectin the body or because the limit was reached and we triggered the action
527+
case responseBodyAccess && !responseBodyProcessPartial:
528+
requireEqualAction(t, types.ActionPause, responseBodyAction, "unexpected request body action, want %q, have %q")
529+
// ProcessPartial: we expect pause until the limit is reached
530+
case responseBodyAccess && responseBodyProcessPartial && totalBodysent < responseBodyLimit:
531+
requireEqualAction(t, types.ActionPause, responseBodyAction, "unexpected request body action, want %q, have %q")
498532
default:
499533
requireEqualAction(t, types.ActionContinue, responseBodyAction, "unexpected response body action, want %q, have %q")
500534
}

wasmplugin/plugin.go

Lines changed: 67 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -384,36 +384,42 @@ func (ctx *httpContext) OnHttpRequestBody(bodySize int, endOfStream bool) types.
384384
return types.ActionContinue
385385
}
386386

387-
if bodySize > 0 {
388-
b, err := proxywasm.GetHttpRequestBody(ctx.bodyReadIndex, bodySize)
389-
if err == nil {
390-
interruption, _, err := tx.WriteRequestBody(b)
391-
if err != nil {
392-
ctx.logger.Error().Err(err).Msg("Failed to write request body")
393-
return types.ActionContinue
394-
}
395-
396-
if interruption != nil {
397-
return ctx.handleInterruption(interruptionPhaseHttpRequestBody, interruption)
398-
}
399-
400-
ctx.bodyReadIndex += bodySize
401-
} else if err != types.ErrorStatusNotFound {
402-
// When using FWT sometimes (it is inconsistent) we receive calls where ctx.bodyReadIndex == bodySize
403-
// meaning that the incoming size in the body is the same as the already read body.
404-
// When that happens, this code fails to retrieve the body through proxywasm.GetHttpRequestBody
405-
// as the total body is from 0 up to X bytes and since the last bodySize = X it attempts to read
406-
// from X up to X bytes and it returns a types.ErrorStatusNotFound. This could happen despite
407-
// endOfStream being true or false.
408-
// The tests in 920410 show this problem.
409-
// TODO(jcchavezs): Verify if this is a FTW problem.
410-
ctx.logger.Error().
411-
Err(err).
412-
Int("body_read_index", ctx.bodyReadIndex).
387+
// bodySize is the size of the whole body received so far, not the size of the current chunk
388+
chunkSize := bodySize - ctx.bodyReadIndex
389+
// OnHttpRequestBody might be called more than once with the same data, we check if there is new data available to be read
390+
if chunkSize > 0 {
391+
bodyChunk, err := proxywasm.GetHttpRequestBody(ctx.bodyReadIndex, chunkSize)
392+
if err != nil {
393+
ctx.logger.Error().Err(err).
413394
Int("body_size", bodySize).
395+
Int("body_read_index", ctx.bodyReadIndex).
396+
Int("chunk_size", chunkSize).
414397
Msg("Failed to read request body")
415398
return types.ActionContinue
416399
}
400+
readchunkSize := len(bodyChunk)
401+
if readchunkSize != chunkSize {
402+
ctx.logger.Warn().Int("read_chunk_size", readchunkSize).Int("chunk_size", chunkSize).Msg("Request chunk size read is different from the computed one")
403+
}
404+
interruption, writtenBytes, err := tx.WriteRequestBody(bodyChunk)
405+
if err != nil {
406+
ctx.logger.Error().Err(err).Msg("Failed to write request body")
407+
return types.ActionContinue
408+
}
409+
if interruption != nil {
410+
return ctx.handleInterruption(interruptionPhaseHttpRequestBody, interruption)
411+
}
412+
413+
// If not the whole chunk has been written, it implicitly means that we reached the waf request body limit.
414+
// Internally ProcessRequestBody has been called and it did not raise any interruption (just checked in the condition above).
415+
if writtenBytes < readchunkSize {
416+
// No further body data will be processed
417+
// Setting processedRequestBody avoid to call more than once ProcessRequestBody
418+
ctx.processedRequestBody = true
419+
return types.ActionContinue
420+
}
421+
422+
ctx.bodyReadIndex += readchunkSize
417423
}
418424

419425
if endOfStream {
@@ -531,6 +537,10 @@ func (ctx *httpContext) OnHttpResponseBody(bodySize int, endOfStream bool) types
531537
return replaceResponseBodyWhenInterrupted(ctx.logger, bodySize)
532538
}
533539

540+
if ctx.processedResponseBody {
541+
return types.ActionContinue
542+
}
543+
534544
if ctx.tx == nil {
535545
return types.ActionContinue
536546
}
@@ -562,33 +572,46 @@ func (ctx *httpContext) OnHttpResponseBody(bodySize int, endOfStream bool) types
562572
return types.ActionContinue
563573
}
564574

565-
if bodySize > 0 {
566-
body, err := proxywasm.GetHttpResponseBody(ctx.bodyReadIndex, bodySize)
567-
if err == nil {
568-
interruption, _, err := tx.WriteResponseBody(body)
569-
if err != nil {
570-
ctx.logger.Error().Err(err).Msg("Failed to write response body")
571-
return types.ActionContinue
572-
}
573-
// bodyReadIndex has to be updated before evaluating the interruption
574-
// it is internally needed to replace the full body if the tx is interrupted
575-
ctx.bodyReadIndex += bodySize
576-
if interruption != nil {
577-
return ctx.handleInterruption(interruptionPhaseHttpResponseBody, interruption)
578-
}
579-
} else if err != types.ErrorStatusNotFound {
575+
chunkSize := bodySize - ctx.bodyReadIndex
576+
if chunkSize > 0 {
577+
bodyChunk, err := proxywasm.GetHttpResponseBody(ctx.bodyReadIndex, chunkSize)
578+
if err != nil {
580579
ctx.logger.Error().
581-
Int("body_read_index", ctx.bodyReadIndex).
582580
Int("body_size", bodySize).
581+
Int("body_read_index", ctx.bodyReadIndex).
582+
Int("chunk_size", chunkSize).
583583
Err(err).
584584
Msg("Failed to read response body")
585585
return types.ActionContinue
586586
}
587+
588+
readchunkSize := len(bodyChunk)
589+
if readchunkSize != chunkSize {
590+
ctx.logger.Warn().Int("read_chunk_size", readchunkSize).Int("chunk_size", chunkSize).Msg("Response chunk size read is different from the computed one")
591+
}
592+
interruption, writtenBytes, err := tx.WriteResponseBody(bodyChunk)
593+
if err != nil {
594+
ctx.logger.Error().Err(err).Msg("Failed to write response body")
595+
return types.ActionContinue
596+
}
597+
// bodyReadIndex has to be updated before evaluating the interruption
598+
// it is internally needed to replace the full body if the transaction is interrupted
599+
ctx.bodyReadIndex += readchunkSize
600+
if interruption != nil {
601+
return ctx.handleInterruption(interruptionPhaseHttpResponseBody, interruption)
602+
}
603+
// If not the whole chunk has been written, it implicitly means that we reached the waf response body limit,
604+
// internally ProcessResponseBody has been called and it did not raise any interruption (just checked in the condition above).
605+
if writtenBytes < readchunkSize {
606+
// no further body data will be processed
607+
ctx.processedResponseBody = true
608+
return types.ActionContinue
609+
}
587610
}
588611

589612
if endOfStream {
590613
// We have already sent response headers, an unauthorized response can not be sent anymore,
591-
// but we can still drop the response to prevent leaking sensitive content.
614+
// but we can still drop the response body to prevent leaking sensitive content.
592615
// The error will also be logged by Coraza.
593616
ctx.processedResponseBody = true
594617
interruption, err := tx.ProcessResponseBody()
@@ -604,7 +627,7 @@ func (ctx *httpContext) OnHttpResponseBody(bodySize int, endOfStream bool) types
604627
return types.ActionContinue
605628
}
606629
// Wait until we see the entire body. It has to be buffered in order to check that it is fully legit
607-
// before sending it downstream
630+
// before sending it downstream (to the client)
608631
return types.ActionPause
609632
}
610633

0 commit comments

Comments
 (0)