diff --git a/buildtools/aho-corasick/Dockerfile b/buildtools/aho-corasick/Dockerfile index 8512c88fb4fa..88e3d24c7b1d 100644 --- a/buildtools/aho-corasick/Dockerfile +++ b/buildtools/aho-corasick/Dockerfile @@ -9,6 +9,7 @@ RUN mkdir -p /aho-corasick && curl -L https://github.com/BurntSushi/aho-corasick WORKDIR /aho-corasick ADD aho-corasick.patch aho-corasick.patch RUN patch -p1 < aho-corasick.patch +ENV RUSTFLAGS "-C target-feature=-crt-static" RUN cargo build --release --target wasm32-wasi CMD ["cp", "target/wasm32-wasi/release/libaho_corasick.a", "/out/libaho_corasick.a"] \ No newline at end of file diff --git a/buildtools/aho-corasick/aho-corasick.patch b/buildtools/aho-corasick/aho-corasick.patch index d782328f8508..f36ac3f13edb 100644 --- a/buildtools/aho-corasick/aho-corasick.patch +++ b/buildtools/aho-corasick/aho-corasick.patch @@ -1,3 +1,12 @@ +diff --git a/.gitignore b/.gitignore +index f1a4d65..d6ff1a3 100644 +--- a/.gitignore ++++ b/.gitignore +@@ -1,3 +1,4 @@ ++.idea + .*.swp + doc + tags diff --git a/Cargo.toml b/Cargo.toml index 610bd4d..55e2f37 100644 --- a/Cargo.toml @@ -12,22 +21,35 @@ index 610bd4d..55e2f37 100644 [features] diff --git a/src/exports.rs b/src/exports.rs new file mode 100644 -index 0000000..f97de6d +index 0000000..29c203d --- /dev/null +++ b/src/exports.rs -@@ -0,0 +1,93 @@ +@@ -0,0 +1,107 @@ +use std::mem::MaybeUninit; +use std::slice; ++use std::str; +use crate::{AhoCorasick, AhoCorasickBuilder, MatchKind}; + +static mut MATCHERS: Vec = Vec::new(); + +#[no_mangle] -+pub extern "C" fn new_matcher(patterns_ptr: usize, patterns_len: usize) -> usize { -+ let patterns_str = ptr_to_string(patterns_ptr, patterns_len); -+ std::mem::forget(&patterns_str); ++pub extern "C" fn new_matcher(patterns_ptr: *mut u8, patterns_len: usize) -> usize { ++ let all_patterns = unsafe { ++ slice::from_raw_parts(patterns_ptr, patterns_len) ++ }; + -+ let patterns = patterns_str.split(' '); ++ let mut patterns = Vec::new(); ++ ++ let mut off = 0; ++ while off < patterns_len { ++ let pattern_len = u32::from_le_bytes([all_patterns[off], all_patterns[off+1], all_patterns[off+2], all_patterns[off+3]]) as usize; ++ off += 4; ++ let pattern = unsafe { ++ str::from_utf8_unchecked(&all_patterns[off..off+pattern_len]) ++ }; ++ patterns.push(pattern); ++ off += pattern_len; ++ } + + let ac = AhoCorasickBuilder::new() + .ascii_case_insensitive(true) @@ -39,6 +61,7 @@ index 0000000..f97de6d + MATCHERS.push(ac); + MATCHERS.len() - 1 + } ++ +} + +#[no_mangle] diff --git a/buildtools/tinygo/Dockerfile b/buildtools/tinygo/Dockerfile index 002c30f9c39c..1ccb528f0128 100644 --- a/buildtools/tinygo/Dockerfile +++ b/buildtools/tinygo/Dockerfile @@ -10,10 +10,11 @@ RUN curl -L https://go.dev/dl/go1.19.1.linux-${TARGETARCH:-amd64}.tar.gz | tar - ENV PATH /go/bin:/root/go/bin:$PATH ENV GOROOT /go -RUN apt-get install -y libclang-14-dev wabt binaryen +RUN apt-get install -y libclang-14-dev wabt binaryen git # https://github.com/tinygo-org/tinygo/commit/9e4e182615cd80303c564f95020e0c3bd10af64a -RUN go install github.com/tinygo-org/tinygo@9e4e182615cd80303c564f95020e0c3bd10af64a - -RUN mkdir -p $(tinygo env TINYGOROOT)/lib/wasi-libc/ && \ - ln -s /wasi-sysroot $(tinygo env TINYGOROOT)/lib/wasi-libc/sysroot +RUN git clone --shallow-submodules --recursive https://github.com/tinygo-org/tinygo --branch dev +WORKDIR /tinygo +RUN git reset --hard 9e4e182615cd80303c564f95020e0c3bd10af64a +RUN go install +RUN make wasi-libc diff --git a/config.go b/config.go index deeb78dbec67..9fdbb22c76ac 100644 --- a/config.go +++ b/config.go @@ -1,4 +1,4 @@ -// Copyright 2022 The OWASP Coraza contributors +// Copyright The OWASP Coraza contributors // SPDX-License-Identifier: Apache-2.0 package main diff --git a/config_test.go b/config_test.go index 8ea6ef7eab8b..11a52b3d5091 100644 --- a/config_test.go +++ b/config_test.go @@ -1,4 +1,4 @@ -// Copyright 2022 The OWASP Coraza contributors +// Copyright The OWASP Coraza contributors // SPDX-License-Identifier: Apache-2.0 package main diff --git a/ftw/Dockerfile b/ftw/Dockerfile index d94e1a97bac5..990925fd9792 100644 --- a/ftw/Dockerfile +++ b/ftw/Dockerfile @@ -7,7 +7,7 @@ WORKDIR /workspace RUN apk update && apk add curl -RUN go install github.com/anuraaga/go-ftw@dev +RUN go install github.com/fzipi/go-ftw@fd953f4f9ddd0f21595be4f48f0b468dda32e801 ADD https://github.com/coreruleset/coreruleset/archive/refs/tags/v4.0.0-rc1.tar.gz /workspace/coreruleset/ RUN cd coreruleset && tar -xf v4.0.0-rc1.tar.gz --strip-components 1 diff --git a/ftw/docker-compose.yml b/ftw/docker-compose.yml index f82f30b14227..d176d7fe7273 100644 --- a/ftw/docker-compose.yml +++ b/ftw/docker-compose.yml @@ -1,6 +1,6 @@ services: httpbin: - image: kennethreitz/httpbin:latest + image: ealen/echo-server:latest chown: image: alpine:3.16 command: @@ -16,7 +16,7 @@ services: image: envoyproxy/envoy:v1.23-latest command: - -c - - /conf/envoy-config.yaml + - ${ENVOY_CONFIG:-/conf/envoy-config.yaml} - --log-level - info - --component-log-level @@ -54,6 +54,8 @@ services: depends_on: - wasm-logs build: . + environment: + - FTW_CLOUDMODE volumes: - logs:/home/envoy/logs:ro volumes: diff --git a/ftw/envoy-config-nowasm.yaml b/ftw/envoy-config-nowasm.yaml new file mode 100644 index 000000000000..ac3a3c357ab1 --- /dev/null +++ b/ftw/envoy-config-nowasm.yaml @@ -0,0 +1,44 @@ +static_resources: + listeners: + - address: + socket_address: + address: 0.0.0.0 + port_value: 80 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + codec_type: auto + http_protocol_options: + accept_http_10: true + route_config: + virtual_hosts: + - name: local_route + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: local_server + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + clusters: + - name: local_server + connect_timeout: 6000s + type: strict_dns + lb_policy: round_robin + load_assignment: + cluster_name: local_server + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: httpbin + port_value: 80 diff --git a/ftw/tests.sh b/ftw/tests.sh index ae8e85605656..244aa69421c8 100755 --- a/ftw/tests.sh +++ b/ftw/tests.sh @@ -29,4 +29,6 @@ while [[ "$status_code" -eq "000" ]]; do done echo -e "\n[Ok] Got status code $status_code, expected 200. Ready to start." -go-ftw run -d coreruleset/tests/regression/tests --config ftw.yml --read-timeout=10s || (echo "Envoy Logs:" && cat /home/envoy/logs/envoy.log) +FTW_CLOUDMODE=${FTW_CLOUDMODE:-false} + +go-ftw run -d coreruleset/tests/regression/tests --config ftw.yml --read-timeout=10s --cloud=$FTW_CLOUDMODE || (echo "Envoy Logs:" && cat /home/envoy/logs/envoy.log) diff --git a/go.mod b/go.mod index 48d1a059a9a9..76cf3caa51ca 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,15 @@ module github.com/jcchavezs/coraza-wasm-filter go 1.17 require ( - github.com/corazawaf/coraza/v3 v3.0.0-20220913021343-a3bd8c85ebf5 + github.com/corazawaf/coraza/v3 v3.0.0-20220928011626-fce26f25ab3e github.com/magefile/mage v1.13.0 github.com/stretchr/testify v1.8.0 - github.com/tetratelabs/proxy-wasm-go-sdk v0.19.1-0.20220831045923-bd6f69563ef4 + github.com/tetratelabs/proxy-wasm-go-sdk v0.19.1-0.20220922045757-132ee0a06ac2 github.com/tidwall/gjson v1.14.3 ) require ( - github.com/corazawaf/libinjection-go v0.0.0-20220909190158-227e7e772cef // indirect + github.com/corazawaf/libinjection-go v0.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/kr/pretty v0.1.0 // indirect github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9 // indirect @@ -19,7 +19,7 @@ require ( github.com/tetratelabs/wazero v1.0.0-beta.2 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect - golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced // indirect + golang.org/x/net v0.0.0-20220909164309-bea034e7d591 // 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 6d2de7d19a58..3e571d7a46a3 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ github.com/anuraaga/go-modsecurity v0.0.0-20220824035035-b9a4099778df/go.mod h1:7jguE759ADzy2EkxGRXigiC0ER1Yq2IFk2qNtwgzc7U= -github.com/corazawaf/coraza/v3 v3.0.0-20220913021343-a3bd8c85ebf5 h1:yfQWquK/A4egSMhxEoQQrMglWnTkXLkkHIhh+SeqPHQ= -github.com/corazawaf/coraza/v3 v3.0.0-20220913021343-a3bd8c85ebf5/go.mod h1:xhc7feR6FUfYgmBmRw3UObvLiyzT3XPQtlJD+huy+Mc= -github.com/corazawaf/libinjection-go v0.0.0-20220909190158-227e7e772cef h1:q9ty0pTOklSWgTasNL1N820pNVcn933BFz5w79DPbbo= -github.com/corazawaf/libinjection-go v0.0.0-20220909190158-227e7e772cef/go.mod h1:OP4TM7xdJ2skyXqNX1AN1wN5nNZEmJNuWbNPOItn7aw= +github.com/corazawaf/coraza/v3 v3.0.0-20220928011626-fce26f25ab3e h1:5EnuiKFLRHct8ZHuzEgPjoAYy21ufDgXt01tqQVFzcg= +github.com/corazawaf/coraza/v3 v3.0.0-20220928011626-fce26f25ab3e/go.mod h1:D89v4pivoxiY7Ij65EryL3ERX7/I/AyRnZEKxkNI4QA= +github.com/corazawaf/libinjection-go v0.1.1 h1:N/SMuy9Q4wPL72pU/OsoYjIIjfvUbsVwHf8A3tWMLKg= +github.com/corazawaf/libinjection-go v0.1.1/go.mod h1:OP4TM7xdJ2skyXqNX1AN1wN5nNZEmJNuWbNPOItn7aw= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -27,8 +27,8 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/tetratelabs/proxy-wasm-go-sdk v0.19.1-0.20220831045923-bd6f69563ef4 h1:FSXp4DrzGamerGY9bhBsL2yJdedDZbMwSsxJEaVxHWo= -github.com/tetratelabs/proxy-wasm-go-sdk v0.19.1-0.20220831045923-bd6f69563ef4/go.mod h1:YVEdbjpYmWRaTXSHuIK6O1kibi313mcQpTbcuBTd4Mg= +github.com/tetratelabs/proxy-wasm-go-sdk v0.19.1-0.20220922045757-132ee0a06ac2 h1:Dd0c7ZCOStoWSnBtfcoF7mXC0MPA9YfbrCHkE4qexZs= +github.com/tetratelabs/proxy-wasm-go-sdk v0.19.1-0.20220922045757-132ee0a06ac2/go.mod h1:YVEdbjpYmWRaTXSHuIK6O1kibi313mcQpTbcuBTd4Mg= github.com/tetratelabs/wazero v1.0.0-beta.2 h1:Qa1R1oizAYHcmy8PljgINdXUZ/nRQkxUBbYfGSb4IBE= github.com/tetratelabs/wazero v1.0.0-beta.2/go.mod h1:CD5smBN5rGZo7UNe8aUiWyYE3bDWED/CQSonog9NSEg= github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw= @@ -53,8 +53,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced h1:3dYNDff0VT5xj+mbj2XucFst9WKk6PdGOrb9n+SbIvw= -golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591 h1:D0B/7al0LLrVC8aWF4+oxpv/m8bc7ViFfVS8/gXGdqI= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -71,8 +71,8 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 h1:v1W7bwXHsnLLloWYTVEdvGvA7BHMeBYsPcF0GLDxIRs= -golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220913175220-63ea55921009 h1:PuvuRMeLWqsf/ZdT1UUZz0syhioyv1mzuFZsXs4fvhw= +golang.org/x/sys v0.0.0-20220913175220-63ea55921009/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/init_tinygo.go b/init_tinygo.go new file mode 100644 index 000000000000..9993a8348de8 --- /dev/null +++ b/init_tinygo.go @@ -0,0 +1,9 @@ +// Copyright The OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +//go:build tinygo + +package main + +// #cgo LDFLAGS: lib/libinjection.a lib/libre2.a lib/libcre2.a lib/libc++.a lib/libc++abi.a lib/libclang_rt.builtins-wasm32.a lib/libaho_corasick.a +import "C" diff --git a/internal/ahocorasick/ahocorasick.go b/internal/ahocorasick/ahocorasick.go index bc8d8cbd833f..3066c445efe1 100644 --- a/internal/ahocorasick/ahocorasick.go +++ b/internal/ahocorasick/ahocorasick.go @@ -1,4 +1,4 @@ -// Copyright 2022 The OWASP Coraza contributors +// Copyright The OWASP Coraza contributors // SPDX-License-Identifier: Apache-2.0 //go:build tinygo @@ -6,8 +6,8 @@ package ahocorasick import ( + "encoding/binary" "reflect" - "runtime" "unsafe" ) @@ -21,10 +21,20 @@ type Matcher struct { ptr uint32 } -func NewMatcher(patternsStr string) Matcher { - sh := (*reflect.StringHeader)(unsafe.Pointer(&patternsStr)) - ac := newMatcher(unsafe.Pointer(sh.Data), uint32(sh.Len)) - runtime.KeepAlive(patternsStr) +func NewMatcher(patterns []string) Matcher { + var bufSize int + for _, p := range patterns { + bufSize += 4 + bufSize += len(p) + } + + buf := make([]byte, 0, bufSize) + for _, p := range patterns { + buf = binary.LittleEndian.AppendUint32(buf, uint32(len(p))) + buf = append(buf, p...) + } + + ac := newMatcher(unsafe.Pointer(&buf[0]), uint32(bufSize)) return Matcher{ptr: ac} } diff --git a/internal/calloc/calloc.go b/internal/calloc/calloc.go index 6f4c1cc474ac..43d270dfb308 100644 --- a/internal/calloc/calloc.go +++ b/internal/calloc/calloc.go @@ -1,4 +1,4 @@ -// Copyright 2022 The OWASP Coraza contributors +// Copyright The OWASP Coraza contributors // SPDX-License-Identifier: Apache-2.0 //go:build tinygo @@ -37,3 +37,9 @@ func posix_memalign(memptr *unsafe.Pointer, alignment, size uintptr) int { *memptr = libc_malloc(size) return 0 } + +//export aligned_alloc +func aligned_alloc(alignment, size uintptr) unsafe.Pointer { + // Ignore alignment for now + return libc_malloc(size) +} diff --git a/internal/calloc/doc.go b/internal/calloc/doc.go index eea318128695..630321c6b543 100644 --- a/internal/calloc/doc.go +++ b/internal/calloc/doc.go @@ -1,4 +1,4 @@ -// Copyright 2022 The OWASP Coraza contributors +// Copyright The OWASP Coraza contributors // SPDX-License-Identifier: Apache-2.0 // Package calloc reimplements some memory allocation functions that C++ expects to exist diff --git a/internal/injection/injection.go b/internal/injection/injection.go index 87da6015a235..8ea262ff629a 100644 --- a/internal/injection/injection.go +++ b/internal/injection/injection.go @@ -1,4 +1,4 @@ -// Copyright 2022 The OWASP Coraza contributors +// Copyright The OWASP Coraza contributors // SPDX-License-Identifier: Apache-2.0 //go:build tinygo diff --git a/internal/operators/from_file.go b/internal/operators/from_file.go new file mode 100644 index 000000000000..40c0f98c4134 --- /dev/null +++ b/internal/operators/from_file.go @@ -0,0 +1,49 @@ +// Copyright The OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +//go:build tinygo + +package operators + +import ( + "errors" + "io/fs" + "os" + "path" +) + +var errEmptyPaths = errors.New("empty paths") + +func loadFromFile(filepath string, paths []string, root fs.FS) ([]byte, error) { + if path.IsAbs(filepath) { + return fs.ReadFile(root, filepath) + } + + if len(paths) == 0 { + return nil, errEmptyPaths + } + + // handling files by operators is hard because we must know the paths where we can + // search, for example, the policy path or the binary path... + // CRS stores the .data files in the same directory as the directives + var ( + content []byte + err error + ) + + for _, p := range paths { + absFilepath := path.Join(p, filepath) + content, err = fs.ReadFile(root, absFilepath) + if err != nil { + if os.IsNotExist(err) { + continue + } else { + return nil, err + } + } + + return content, nil + } + + return nil, err +} diff --git a/internal/operators/operators.go b/internal/operators/operators.go index b809e20411cf..3ee747f62863 100644 --- a/internal/operators/operators.go +++ b/internal/operators/operators.go @@ -1,4 +1,4 @@ -// Copyright 2022 The OWASP Coraza contributors +// Copyright The OWASP Coraza contributors // SPDX-License-Identifier: Apache-2.0 //go:build tinygo @@ -6,12 +6,14 @@ package operators import ( - "github.com/corazawaf/coraza/v3" "github.com/corazawaf/coraza/v3/operators" + "github.com/corazawaf/coraza/v3/rules" ) func Register() { - operators.Register("detectSQLi", func() coraza.RuleOperator { return &detectSQLi{} }) - operators.Register("detectXSS", func() coraza.RuleOperator { return &detectXSS{} }) - operators.Register("rx", func() coraza.RuleOperator { return &rx{} }) + operators.Register("detectSQLi", func() rules.Operator { return &detectSQLi{} }) + operators.Register("detectXSS", func() rules.Operator { return &detectXSS{} }) + operators.Register("rx", func() rules.Operator { return &rx{} }) + operators.Register("pm", func() rules.Operator { return &pm{} }) + operators.Register("pmFromFile", func() rules.Operator { return &pmFromFile{} }) } diff --git a/internal/operators/operators_go.go b/internal/operators/operators_go.go index 201ec752042e..f25a1dd2c2c0 100644 --- a/internal/operators/operators_go.go +++ b/internal/operators/operators_go.go @@ -1,4 +1,4 @@ -// Copyright 2022 The OWASP Coraza contributors +// Copyright The OWASP Coraza contributors // SPDX-License-Identifier: Apache-2.0 //go:build !tinygo diff --git a/internal/operators/pm.go b/internal/operators/pm.go index 0aae7c6cfa6d..6576293cb7f1 100644 --- a/internal/operators/pm.go +++ b/internal/operators/pm.go @@ -1,4 +1,4 @@ -// Copyright 2022 The OWASP Coraza contributors +// Copyright The OWASP Coraza contributors // SPDX-License-Identifier: Apache-2.0 //go:build tinygo @@ -6,7 +6,9 @@ package operators import ( - "github.com/corazawaf/coraza/v3" + "strings" + + "github.com/corazawaf/coraza/v3/rules" "github.com/jcchavezs/coraza-wasm-filter/internal/ahocorasick" ) @@ -15,16 +17,20 @@ type pm struct { m ahocorasick.Matcher } -var _ coraza.RuleOperator = (*pm)(nil) +var _ rules.Operator = (*pm)(nil) -func (o *pm) Init(options coraza.RuleOperatorOptions) error { - o.m = ahocorasick.NewMatcher(options.Arguments) +func (o *pm) Init(options rules.OperatorOptions) error { + o.m = ahocorasick.NewMatcher(strings.Split(options.Arguments, " ")) return nil } -func (o *pm) Evaluate(tx *coraza.Transaction, value string) bool { - matches := o.m.Matches(value, 8) - if tx.Capture { +func (o *pm) Evaluate(tx rules.TransactionState, value string) bool { + return pmEvaluate(o.m, tx, value) +} + +func pmEvaluate(m ahocorasick.Matcher, tx rules.TransactionState, value string) bool { + matches := m.Matches(value, 8) + if tx.Capturing() { for i, c := range matches { tx.CaptureField(i, c) } diff --git a/internal/operators/pm_from_file.go b/internal/operators/pm_from_file.go new file mode 100644 index 000000000000..baa77886af20 --- /dev/null +++ b/internal/operators/pm_from_file.go @@ -0,0 +1,52 @@ +// Copyright The OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +//go:build tinygo + +package operators + +import ( + "bufio" + "bytes" + "strings" + + "github.com/corazawaf/coraza/v3/rules" + + "github.com/jcchavezs/coraza-wasm-filter/internal/ahocorasick" +) + +type pmFromFile struct { + m ahocorasick.Matcher +} + +var _ rules.Operator = (*pmFromFile)(nil) + +func (o *pmFromFile) Init(options rules.OperatorOptions) error { + path := options.Arguments + + data, err := loadFromFile(path, options.Path, options.Root) + if err != nil { + return err + } + + var lines []string + sc := bufio.NewScanner(bytes.NewReader(data)) + for sc.Scan() { + l := sc.Text() + l = strings.TrimSpace(l) + if len(l) == 0 { + continue + } + if l[0] == '#' { + continue + } + lines = append(lines, strings.ToLower(l)) + } + + o.m = ahocorasick.NewMatcher(lines) + return nil +} + +func (o *pmFromFile) Evaluate(tx rules.TransactionState, value string) bool { + return pmEvaluate(o.m, tx, value) +} diff --git a/internal/operators/rx.go b/internal/operators/rx.go index 791f2ce0a186..df0e58c1b137 100644 --- a/internal/operators/rx.go +++ b/internal/operators/rx.go @@ -1,4 +1,4 @@ -// Copyright 2022 The OWASP Coraza contributors +// Copyright The OWASP Coraza contributors // SPDX-License-Identifier: Apache-2.0 //go:build tinygo @@ -6,29 +6,39 @@ package operators import ( - "github.com/corazawaf/coraza/v3" + "fmt" + + "github.com/corazawaf/coraza/v3/rules" "github.com/jcchavezs/coraza-wasm-filter/internal/re2" ) type rx struct { - re re2.RegExp + re re2.RegExp + debug bool } -var _ coraza.RuleOperator = (*rx)(nil) +var _ rules.Operator = (*rx)(nil) -func (o *rx) Init(options coraza.RuleOperatorOptions) error { +func (o *rx) Init(options rules.OperatorOptions) error { data := options.Arguments + // fmt.Println(data) + if data == `(?:\$(?:\((?:\(.*\)|.*)\)|\{.*})|\/\w*\[!?.+\]|[<>]\(.*\))` { + o.debug = true + fmt.Println("enabling rx debug!") + } re, err := re2.Compile(data) o.re = re return err } -func (o *rx) Evaluate(tx *coraza.Transaction, value string) bool { - return o.re.FindStringSubmatch8(value, func(i int, match string) { - if tx.Capture { - tx.CaptureField(i, match) - } +func (o *rx) Evaluate(tx rules.TransactionState, value string) bool { + res := o.re.FindStringSubmatch8(value, func(i int, match string) { + tx.CaptureField(i, match) }) + if o.debug { + fmt.Println(res) + } + return res } diff --git a/internal/operators/sqli.go b/internal/operators/sqli.go index 8f9202b3e9b3..63f701f96464 100644 --- a/internal/operators/sqli.go +++ b/internal/operators/sqli.go @@ -1,4 +1,4 @@ -// Copyright 2022 The OWASP Coraza contributors +// Copyright The OWASP Coraza contributors // SPDX-License-Identifier: Apache-2.0 //go:build tinygo @@ -6,7 +6,7 @@ package operators import ( - "github.com/corazawaf/coraza/v3" + "github.com/corazawaf/coraza/v3/rules" "github.com/jcchavezs/coraza-wasm-filter/internal/injection" ) @@ -14,17 +14,15 @@ import ( type detectSQLi struct { } -var _ coraza.RuleOperator = (*detectSQLi)(nil) +var _ rules.Operator = (*detectSQLi)(nil) -func (o *detectSQLi) Init(options coraza.RuleOperatorOptions) error { return nil } +func (o *detectSQLi) Init(options rules.OperatorOptions) error { return nil } -func (o *detectSQLi) Evaluate(tx *coraza.Transaction, value string) bool { +func (o *detectSQLi) Evaluate(tx rules.TransactionState, value string) bool { res, fp := injection.IsSQLi(value) if !res { return false } - if tx.Capture { - tx.CaptureField(0, string(fp)) - } + tx.CaptureField(0, string(fp)) return true } diff --git a/internal/operators/xss.go b/internal/operators/xss.go index f3b527c5152d..66dde362ee85 100644 --- a/internal/operators/xss.go +++ b/internal/operators/xss.go @@ -1,4 +1,4 @@ -// Copyright 2022 The OWASP Coraza contributors +// Copyright The OWASP Coraza contributors // SPDX-License-Identifier: Apache-2.0 //go:build tinygo @@ -6,7 +6,7 @@ package operators import ( - "github.com/corazawaf/coraza/v3" + "github.com/corazawaf/coraza/v3/rules" "github.com/jcchavezs/coraza-wasm-filter/internal/injection" ) @@ -14,10 +14,10 @@ import ( type detectXSS struct { } -var _ coraza.RuleOperator = (*detectXSS)(nil) +var _ rules.Operator = (*detectXSS)(nil) -func (o *detectXSS) Init(options coraza.RuleOperatorOptions) error { return nil } +func (o *detectXSS) Init(options rules.OperatorOptions) error { return nil } -func (o *detectXSS) Evaluate(tx *coraza.Transaction, value string) bool { +func (o *detectXSS) Evaluate(tx rules.TransactionState, value string) bool { return injection.IsXSS(value) } diff --git a/internal/re2/re2.go b/internal/re2/re2.go index b96e4d98bf2b..cfeb8ed3d61e 100644 --- a/internal/re2/re2.go +++ b/internal/re2/re2.go @@ -1,4 +1,4 @@ -// Copyright 2022 The OWASP Coraza contributors +// Copyright The OWASP Coraza contributors // SPDX-License-Identifier: Apache-2.0 //go:build tinygo diff --git a/lib/libaho_corasick.a b/lib/libaho_corasick.a index c6db3cbb546d..04c6a69d022b 100644 Binary files a/lib/libaho_corasick.a and b/lib/libaho_corasick.a differ diff --git a/logger.go b/logger.go index 14adcad60fcd..87ce8fcc4abc 100644 --- a/logger.go +++ b/logger.go @@ -1,4 +1,4 @@ -// Copyright 2022 The OWASP Coraza contributors +// Copyright The OWASP Coraza contributors // SPDX-License-Identifier: Apache-2.0 package main @@ -6,45 +6,45 @@ package main import ( "io" - "github.com/corazawaf/coraza/v3" + "github.com/corazawaf/coraza/v3/loggers" "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm" ) type debugLogger struct { - level coraza.LogLevel + level loggers.LogLevel } func (l *debugLogger) Info(message string, args ...interface{}) { - if l.level >= coraza.LogLevelInfo { + if l.level >= loggers.LogLevelInfo { proxywasm.LogInfof(message, args...) } } func (l *debugLogger) Warn(message string, args ...interface{}) { - if l.level >= coraza.LogLevelWarn { + if l.level >= loggers.LogLevelWarn { proxywasm.LogWarnf(message, args...) } } func (l *debugLogger) Error(message string, args ...interface{}) { - if l.level >= coraza.LogLevelError { + if l.level >= loggers.LogLevelError { proxywasm.LogErrorf(message, args...) } } func (l *debugLogger) Debug(message string, args ...interface{}) { - if l.level >= coraza.LogLevelDebug { + if l.level >= loggers.LogLevelDebug { proxywasm.LogDebugf(message, args...) } } func (l *debugLogger) Trace(message string, args ...interface{}) { - if l.level >= coraza.LogLevelTrace { + if l.level >= loggers.LogLevelTrace { proxywasm.LogTracef(message, args...) } } -func (l *debugLogger) SetLevel(level coraza.LogLevel) { +func (l *debugLogger) SetLevel(level loggers.LogLevel) { l.level = level } diff --git a/mage.go b/mage.go index b48cb5d98671..7b80ab776f69 100644 --- a/mage.go +++ b/mage.go @@ -1,4 +1,4 @@ -// Copyright 2022 The OWASP Coraza contributors +// Copyright The OWASP Coraza contributors // SPDX-License-Identifier: Apache-2.0 //go:build ignore diff --git a/magefile.go b/magefile.go index a585fcf4cbb4..777df99cbbe3 100644 --- a/magefile.go +++ b/magefile.go @@ -1,4 +1,4 @@ -// Copyright 2022 The OWASP Coraza contributors +// Copyright The OWASP Coraza contributors // SPDX-License-Identifier: Apache-2.0 //go:build mage @@ -18,9 +18,9 @@ import ( "github.com/magefile/mage/sh" ) -var addLicenseVersion = "v1.0.0" // https://github.com/google/addlicense -var golangCILintVer = "v1.48.0" // https://github.com/golangci/golangci-lint/releases -var gosImportsVer = "v0.3.1" // https://github.com/rinchsan/gosimports/releases/tag/v0.3.1 +var addLicenseVersion = "04bfe4ee9ca5764577b029acc6a1957fd1997153" // https://github.com/google/addlicense +var golangCILintVer = "v1.48.0" // https://github.com/golangci/golangci-lint/releases +var gosImportsVer = "v0.3.1" // https://github.com/rinchsan/gosimports/releases/tag/v0.3.1 var errCommitFormatting = errors.New("files not formatted, please commit formatting changes") var errNoGitDir = errors.New("no .git directory found") @@ -35,6 +35,7 @@ func Format() error { if _, err := sh.Exec(map[string]string{}, io.Discard, io.Discard, "go", "run", fmt.Sprintf("github.com/google/addlicense@%s", addLicenseVersion), "-c", "The OWASP Coraza contributors", "-s=only", + "-y=", "-ignore", "**/*.yml", "-ignore", "**/*.yaml", "-ignore", "examples/**", "."); err != nil { @@ -113,12 +114,17 @@ func Build() error { return err } - script := ` + timingBuildTag := "" + if os.Getenv("TIMING") == "true" { + timingBuildTag = "-tags 'timing proxywasm_timing'" + } + + script := fmt.Sprintf(` cd /src && \ -tinygo build -opt 2 -o build/mainraw.wasm -scheduler=none -target=wasi . && \ +tinygo build -opt 2 -o build/mainraw.wasm -scheduler=none -target=wasi %s . && \ wasm-opt -Os -c build/mainraw.wasm -o build/mainopt.wasm && \ wasm2wat --enable-all build/mainopt.wasm -o build/mainopt.wat -` +`, timingBuildTag) if err := sh.RunV("docker", "run", "--pull", "always", "--rm", "-v", fmt.Sprintf("%s:/src", wd), "ghcr.io/anuraaga/coraza-wasm-filter/buildtools-tinygo:main", "bash", "-c", strings.TrimSpace(script)); err != nil { @@ -137,7 +143,7 @@ wasm2wat --enable-all build/mainopt.wasm -o build/mainopt.wat return err } return sh.RunV("docker", "run", "--rm", "-v", fmt.Sprintf("%s:/build", filepath.Join(wd, "build")), "ghcr.io/anuraaga/coraza-wasm-filter/buildtools-tinygo:main", "bash", "-c", - "wat2wasm --enable-all build/main.wat -o build/main.wasm") + "wat2wasm --enable-all /build/main.wat -o /build/main.wasm") } func UpdateLibs() error { @@ -170,7 +176,13 @@ func Ftw() error { defer func() { _ = sh.RunV("docker-compose", "--file", "ftw/docker-compose.yml", "down", "-v") }() - return sh.RunV("docker-compose", "--file", "ftw/docker-compose.yml", "run", "--rm", "ftw") + env := map[string]string{ + "FTW_CLOUDMODE": os.Getenv("FTW_CLOUDMODE"), + } + if os.Getenv("ENVOY_NOWASM") == "true" { + env["ENVOY_CONFIG"] = "/conf/envoy-config-nowasm.yaml" + } + return sh.RunWithV(env, "docker-compose", "--file", "ftw/docker-compose.yml", "run", "--rm", "ftw") } var Default = Build diff --git a/main.go b/main.go index 6132f2ba198d..6ac99b544cb7 100644 --- a/main.go +++ b/main.go @@ -1,4 +1,4 @@ -// Copyright 2022 The OWASP Coraza contributors +// Copyright The OWASP Coraza contributors // SPDX-License-Identifier: Apache-2.0 package main @@ -10,7 +10,6 @@ import ( "strconv" "github.com/corazawaf/coraza/v3" - "github.com/corazawaf/coraza/v3/seclang" ctypes "github.com/corazawaf/coraza/v3/types" "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm" "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types" @@ -19,9 +18,6 @@ import ( "github.com/jcchavezs/coraza-wasm-filter/internal/operators" ) -// #cgo LDFLAGS: lib/libinjection.a lib/libre2.a lib/libcre2.a lib/libc++.a lib/libc++abi.a lib/libclang_rt.builtins-wasm32.a lib/libaho_corasick.a -import "C" - //go:embed rules var crs embed.FS @@ -46,7 +42,7 @@ type corazaPlugin struct { // so that we don't need to reimplement all the methods. types.DefaultPluginContext - waf *coraza.WAF + waf coraza.WAF } // Override types.DefaultPluginContext. @@ -62,19 +58,19 @@ func (ctx *corazaPlugin) OnPluginStart(pluginConfigurationSize int) types.OnPlug return types.OnPluginStartStatusFailed } - // First we initialize our waf and our seclang parser - waf := coraza.NewWAF() - waf.SetErrorLogCb(logError) - waf.Logger = &debugLogger{} - - // TinyGo compilation will prevent buffering request body to files anyways, so this is - // effectively no-op but make clear our expectations. - // TODO(anuraaga): Make this configurable in plugin configuration. - waf.RequestBodyLimit = waf.RequestBodyInMemoryLimit - - parser := seclang.NewParser(waf) root, _ := fs.Sub(crs, "rules") - parser.SetRoot(root) + + // First we initialize our waf and our seclang parser + conf := coraza.NewWAFConfig(). + WithErrorLogger(logError). + WithDebugLogger(&debugLogger{}). + WithRequestBodyAccess(coraza.NewRequestBodyConfig(). + WithLimit(1024 * 1024 * 1024). + // TinyGo compilation will prevent buffering request body to files anyways, so this is + // effectively no-op but make clear our expectations. + // TODO(anuraaga): Make this configurable in plugin configuration. + WithInMemoryLimit(1024 * 1024 * 1024)). + WithRootFS(root) crs, err := fs.Sub(crs, "custom_rules") if err != nil { @@ -88,7 +84,9 @@ func (ctx *corazaPlugin) OnPluginStart(pluginConfigurationSize int) types.OnPlug return types.OnPluginStartStatusFailed } - err = parser.FromString(rules) + conf = conf.WithDirectives(rules) + + waf, err := coraza.NewWAF(conf) if err != nil { proxywasm.LogCriticalf("failed to parse rules: %v", err) return types.OnPluginStartStatusFailed @@ -109,7 +107,7 @@ type httpContext struct { // so that we don't need to reimplement all the methods. types.DefaultHttpContext contextID uint32 - tx *coraza.Transaction + tx ctypes.Transaction httpProtocol string processedRequestBody bool processedResponseBody bool @@ -117,6 +115,7 @@ type httpContext struct { // Override types.DefaultHttpContext. func (ctx *httpContext) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action { + defer logTime("OnHttpRequestHeaders", currentTime()) tx := ctx.tx // This currently relies on Envoy's behavior of mapping all requests to HTTP/2 semantics @@ -171,6 +170,7 @@ func (ctx *httpContext) OnHttpRequestHeaders(numHeaders int, endOfStream bool) t } func (ctx *httpContext) OnHttpRequestBody(bodySize int, endOfStream bool) types.Action { + defer logTime("OnHttpRequestBody", currentTime()) tx := ctx.tx if bodySize > 0 { @@ -180,7 +180,7 @@ func (ctx *httpContext) OnHttpRequestBody(bodySize int, endOfStream bool) types. return types.ActionContinue } - _, err = tx.RequestBodyBuffer.Write(body) + _, err = tx.RequestBodyWriter().Write(body) if err != nil { proxywasm.LogCriticalf("failed to read request body: %v", err) return types.ActionContinue @@ -205,6 +205,7 @@ func (ctx *httpContext) OnHttpRequestBody(bodySize int, endOfStream bool) types. } func (ctx *httpContext) OnHttpResponseHeaders(numHeaders int, endOfStream bool) types.Action { + defer logTime("OnHttpResponseHeaders", currentTime()) tx := ctx.tx // Requests without body won't call OnHttpRequestBody, but there are rules in the request body @@ -250,6 +251,7 @@ func (ctx *httpContext) OnHttpResponseHeaders(numHeaders int, endOfStream bool) } func (ctx *httpContext) OnHttpResponseBody(bodySize int, endOfStream bool) types.Action { + defer logTime("OnHttpResponseBody", currentTime()) tx := ctx.tx if bodySize > 0 { @@ -259,7 +261,7 @@ func (ctx *httpContext) OnHttpResponseBody(bodySize int, endOfStream bool) types return types.ActionContinue } - _, err = tx.ResponseBodyBuffer.Write(body) + _, err = tx.ResponseBodyWriter().Write(body) if err != nil { proxywasm.LogCriticalf("failed to read response body: %v", err) return types.ActionContinue @@ -284,6 +286,7 @@ func (ctx *httpContext) OnHttpResponseBody(bodySize int, endOfStream bool) types // Override types.DefaultHttpContext. func (ctx *httpContext) OnHttpStreamDone() { + defer logTime("OnHttpStreamDone", currentTime()) tx := ctx.tx // Responses without body won't call OnHttpResponseBody, but there are rules in the response body @@ -297,7 +300,7 @@ func (ctx *httpContext) OnHttpStreamDone() { } ctx.tx.ProcessLogging() - _ = ctx.tx.Clean() + _ = ctx.tx.Close() proxywasm.LogInfof("%d finished", ctx.contextID) } diff --git a/main_test.go b/main_test.go index f0988a28dad5..583213eb65a8 100644 --- a/main_test.go +++ b/main_test.go @@ -1,4 +1,4 @@ -// Copyright 2022 The OWASP Coraza contributors +// Copyright The OWASP Coraza contributors // SPDX-License-Identifier: Apache-2.0 package main @@ -715,7 +715,7 @@ func vmTest(t *testing.T, f func(*testing.T, types.VMContext)) { }) t.Run("wasm", func(t *testing.T) { - buildPath := filepath.Join("build", "main.wasm") + buildPath := filepath.Join("build", "mainraw.wasm") wasm, err := os.ReadFile(buildPath) if err != nil { t.Skip("wasm not found") diff --git a/rules/ftw-config.conf b/rules/ftw-config.conf index a8cc3811490d..098a0b968d90 100644 --- a/rules/ftw-config.conf +++ b/rules/ftw-config.conf @@ -10,7 +10,7 @@ SecAction "id:900005,\ setvar:tx.arg_length=400,\ setvar:tx.total_arg_length=64000,\ setvar:tx.max_num_args=255,\ - setvar:tx.combined_file_sizes=65535 + setvar:tx.combined_file_sizes=65535" # Write the value from the X-CRS-Test header as a marker to the log SecRule REQUEST_HEADERS:X-CRS-Test "@rx ^.*$" \ diff --git a/timing.go b/timing.go new file mode 100644 index 000000000000..80bfb35b1623 --- /dev/null +++ b/timing.go @@ -0,0 +1,18 @@ +// Copyright The OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +//go:build !timing + +package main + +import ( + "time" +) + +func currentTime() time.Time { + return time.Time{} +} + +func logTime(msg string, start time.Time) { + // no-op without build tag +} diff --git a/timing_on.go b/timing_on.go new file mode 100644 index 000000000000..be3014b4ce70 --- /dev/null +++ b/timing_on.go @@ -0,0 +1,20 @@ +// Copyright The OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +//go:build timing + +package main + +import ( + "time" + + "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm" +) + +func currentTime() time.Time { + return time.Now() +} + +func logTime(msg string, start time.Time) { + proxywasm.LogDebugf("%s took %s", msg, time.Since(start)) +}