From a95ea5fa9e9bc1b64adfbbb371dac13301c2521d Mon Sep 17 00:00:00 2001 From: Romain Menke <11521496+romainmenke@users.noreply.github.com> Date: Tue, 21 Nov 2023 06:27:48 +0100 Subject: [PATCH 01/12] fix `http.Flusher` and `io.ReaderFrom` implementation (#923) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Carlos Chávez --- http/interceptor.go | 114 ++++------------ http/interceptor_test.go | 282 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 306 insertions(+), 90 deletions(-) diff --git a/http/interceptor.go b/http/interceptor.go index 6b14aead3..a7b616ab5 100644 --- a/http/interceptor.go +++ b/http/interceptor.go @@ -108,7 +108,23 @@ func (i *rwInterceptor) Header() http.Header { return i.w.Header() } -var _ http.ResponseWriter = (*rwInterceptor)(nil) +func (i *rwInterceptor) ReadFrom(r io.Reader) (n int64, err error) { + return io.Copy(i, r) +} + +func (i *rwInterceptor) Flush() { + if !i.wroteHeader { + i.WriteHeader(http.StatusOK) + } +} + +type responseWriter interface { + http.ResponseWriter + io.ReaderFrom + http.Flusher +} + +var _ responseWriter = (*rwInterceptor)(nil) // wrap wraps the interceptor into a response writer that also preserves // the http interfaces implemented by the original response writer to avoid @@ -168,110 +184,28 @@ func wrap(w http.ResponseWriter, r *http.Request, tx types.Transaction) ( var ( hijacker, isHijacker = i.w.(http.Hijacker) pusher, isPusher = i.w.(http.Pusher) - flusher, isFlusher = i.w.(http.Flusher) - reader, isReader = i.w.(io.ReaderFrom) ) switch { - case !isHijacker && !isPusher && !isFlusher && !isReader: + case !isHijacker && isPusher: return struct { - http.ResponseWriter - }{i}, responseProcessor - case !isHijacker && !isPusher && !isFlusher && isReader: - return struct { - http.ResponseWriter - io.ReaderFrom - }{i, reader}, responseProcessor - case !isHijacker && !isPusher && isFlusher && !isReader: - return struct { - http.ResponseWriter - http.Flusher - }{i, flusher}, responseProcessor - case !isHijacker && !isPusher && isFlusher && isReader: - return struct { - http.ResponseWriter - http.Flusher - io.ReaderFrom - }{i, flusher, reader}, responseProcessor - case !isHijacker && isPusher && !isFlusher && !isReader: - return struct { - http.ResponseWriter + responseWriter http.Pusher }{i, pusher}, responseProcessor - case !isHijacker && isPusher && !isFlusher && isReader: - return struct { - http.ResponseWriter - http.Pusher - io.ReaderFrom - }{i, pusher, reader}, responseProcessor - case !isHijacker && isPusher && isFlusher && !isReader: - return struct { - http.ResponseWriter - http.Pusher - http.Flusher - }{i, pusher, flusher}, responseProcessor - case !isHijacker && isPusher && isFlusher && isReader: + case isHijacker && !isPusher: return struct { - http.ResponseWriter - http.Pusher - http.Flusher - io.ReaderFrom - }{i, pusher, flusher, reader}, responseProcessor - case isHijacker && !isPusher && !isFlusher && !isReader: - return struct { - http.ResponseWriter + responseWriter http.Hijacker }{i, hijacker}, responseProcessor - case isHijacker && !isPusher && !isFlusher && isReader: + case isHijacker && isPusher: return struct { - http.ResponseWriter - http.Hijacker - io.ReaderFrom - }{i, hijacker, reader}, responseProcessor - case isHijacker && !isPusher && isFlusher && !isReader: - return struct { - http.ResponseWriter - http.Hijacker - http.Flusher - }{i, hijacker, flusher}, responseProcessor - case isHijacker && !isPusher && isFlusher && isReader: - return struct { - http.ResponseWriter - http.Hijacker - http.Flusher - io.ReaderFrom - }{i, hijacker, flusher, reader}, responseProcessor - case isHijacker && isPusher && !isFlusher && !isReader: - return struct { - http.ResponseWriter + responseWriter http.Hijacker http.Pusher }{i, hijacker, pusher}, responseProcessor - case isHijacker && isPusher && !isFlusher && isReader: - return struct { - http.ResponseWriter - http.Hijacker - http.Pusher - io.ReaderFrom - }{i, hijacker, pusher, reader}, responseProcessor - case isHijacker && isPusher && isFlusher && !isReader: - return struct { - http.ResponseWriter - http.Hijacker - http.Pusher - http.Flusher - }{i, hijacker, pusher, flusher}, responseProcessor - case isHijacker && isPusher && isFlusher && isReader: - return struct { - http.ResponseWriter - http.Hijacker - http.Pusher - http.Flusher - io.ReaderFrom - }{i, hijacker, pusher, flusher, reader}, responseProcessor default: return struct { - http.ResponseWriter + responseWriter }{i}, responseProcessor } } diff --git a/http/interceptor_test.go b/http/interceptor_test.go index e8424705b..e4da8e700 100644 --- a/http/interceptor_test.go +++ b/http/interceptor_test.go @@ -8,6 +8,10 @@ package http import ( + "bufio" + "bytes" + "io" + "net" "net/http" "net/http/httptest" "testing" @@ -44,3 +48,281 @@ func TestWriteHeader(t *testing.T) { t.Errorf("unexpected status code, want %d, have %d", want, have) } } + +func TestWrite(t *testing.T) { + waf, err := coraza.NewWAF(coraza.NewWAFConfig()) + if err != nil { + t.Fatal(err) + } + + tx := waf.NewTransaction() + req, _ := http.NewRequest("GET", "", nil) + res := httptest.NewRecorder() + + rw, responseProcessor := wrap(res, req, tx) + _, err = rw.Write([]byte("hello")) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + _, err = rw.Write([]byte("world")) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + err = responseProcessor(tx, req) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if want, have := 200, res.Code; want != have { + t.Errorf("unexpected status code, want %d, have %d", want, have) + } +} + +func TestWriteWithWriteHeader(t *testing.T) { + waf, err := coraza.NewWAF(coraza.NewWAFConfig()) + if err != nil { + t.Fatal(err) + } + + tx := waf.NewTransaction() + req, _ := http.NewRequest("GET", "", nil) + res := httptest.NewRecorder() + + rw, responseProcessor := wrap(res, req, tx) + rw.WriteHeader(204) + // although we called WriteHeader, status code should be applied until + // responseProcessor is called. + if unwanted, have := 204, res.Code; unwanted == have { + t.Errorf("unexpected status code %d", have) + } + + _, err = rw.Write([]byte("hello")) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + _, err = rw.Write([]byte("world")) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + err = responseProcessor(tx, req) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if want, have := 204, res.Code; want != have { + t.Errorf("unexpected status code, want %d, have %d", want, have) + } +} + +func TestFlush(t *testing.T) { + waf, err := coraza.NewWAF(coraza.NewWAFConfig()) + if err != nil { + t.Fatal(err) + } + + t.Run("WriteHeader before Flush", func(t *testing.T) { + tx := waf.NewTransaction() + req, _ := http.NewRequest("GET", "", nil) + res := httptest.NewRecorder() + rw, responseProcessor := wrap(res, req, tx) + rw.WriteHeader(204) + rw.(http.Flusher).Flush() + // although we called WriteHeader, status code should be applied until + // responseProcessor is called. + if unwanted, have := 204, res.Code; unwanted == have { + t.Errorf("unexpected status code %d", have) + } + + err = responseProcessor(tx, req) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if want, have := 204, res.Code; want != have { + t.Errorf("unexpected status code, want %d, have %d", want, have) + } + }) + + t.Run("Flush before WriteHeader", func(t *testing.T) { + tx := waf.NewTransaction() + req, _ := http.NewRequest("GET", "", nil) + res := httptest.NewRecorder() + rw, responseProcessor := wrap(res, req, tx) + rw.(http.Flusher).Flush() + rw.WriteHeader(204) + + if want, have := 200, res.Code; want != have { + t.Errorf("unexpected status code, want %d, have %d", want, have) + } + + err = responseProcessor(tx, req) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if want, have := 200, res.Code; want != have { + t.Errorf("unexpected status code, want %d, have %d", want, have) + } + }) +} + +type testReaderFrom struct { + io.Writer +} + +func (x *testReaderFrom) ReadFrom(r io.Reader) (n int64, err error) { + return io.Copy(x, r) +} + +func TestReadFrom(t *testing.T) { + waf, err := coraza.NewWAF(coraza.NewWAFConfig()) + if err != nil { + t.Fatal(err) + } + + tx := waf.NewTransaction() + req, _ := http.NewRequest("GET", "", nil) + res := httptest.NewRecorder() + + type responseWriter interface { + http.ResponseWriter + http.Flusher + } + + resWithReaderFrom := struct { + responseWriter + io.ReaderFrom + }{ + res, + &testReaderFrom{res}, + } + + rw, responseProcessor := wrap(resWithReaderFrom, req, tx) + rw.WriteHeader(204) + // although we called WriteHeader, status code should be applied until + // responseProcessor is called. + if unwanted, have := 204, res.Code; unwanted == have { + t.Errorf("unexpected status code %d", have) + } + + _, err = rw.(io.ReaderFrom).ReadFrom(bytes.NewBuffer([]byte("hello world"))) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + err = responseProcessor(tx, req) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if want, have := 204, res.Code; want != have { + t.Errorf("unexpected status code, want %d, have %d", want, have) + } +} + +type testPusher struct{} + +func (x *testPusher) Push(string, *http.PushOptions) error { + return nil +} + +type testHijacker struct{} + +func (x *testHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) { + return nil, nil, nil +} + +func TestInterface(t *testing.T) { + waf, err := coraza.NewWAF(coraza.NewWAFConfig()) + if err != nil { + t.Fatal(err) + } + + tx := waf.NewTransaction() + req, _ := http.NewRequest("GET", "", nil) + res := httptest.NewRecorder() + + t.Run("default", func(t *testing.T) { + rw, _ := wrap(struct { + http.ResponseWriter + }{ + res, + }, req, tx) + + _, ok := rw.(http.Pusher) + if ok { + t.Errorf("expected the wrapped ResponseWriter to not implement http.Pusher") + } + + _, ok = rw.(http.Hijacker) + if ok { + t.Errorf("expected the wrapped ResponseWriter to not implement http.Hijacker") + } + }) + + t.Run("http.Pusher", func(t *testing.T) { + rw, _ := wrap(struct { + http.ResponseWriter + http.Pusher + }{ + res, + &testPusher{}, + }, req, tx) + + _, ok := rw.(http.Pusher) + if !ok { + t.Errorf("expected the wrapped ResponseWriter to implement http.Pusher") + } + + _, ok = rw.(http.Hijacker) + if ok { + t.Errorf("expected the wrapped ResponseWriter to not implement http.Hijacker") + } + }) + + t.Run("http.Hijacker", func(t *testing.T) { + rw, _ := wrap(struct { + http.ResponseWriter + http.Hijacker + }{ + res, + &testHijacker{}, + }, req, tx) + + _, ok := rw.(http.Hijacker) + if !ok { + t.Errorf("expected the wrapped ResponseWriter to implement http.Hijacker") + } + + _, ok = rw.(http.Pusher) + if ok { + t.Errorf("expected the wrapped ResponseWriter to not implement http.Pusher") + } + }) + + t.Run("http.Hijacker and http.Pusher", func(t *testing.T) { + rw, _ := wrap(struct { + http.ResponseWriter + http.Hijacker + http.Pusher + }{ + res, + &testHijacker{}, + &testPusher{}, + }, req, tx) + + _, ok := rw.(http.Hijacker) + if !ok { + t.Errorf("expected the wrapped ResponseWriter to implement http.Hijacker") + } + + _, ok = rw.(http.Pusher) + if !ok { + t.Errorf("expected the wrapped ResponseWriter to implement http.Pusher") + } + }) +} From 4c7f91822b985ff43bae8922a2c4ab9763db1055 Mon Sep 17 00:00:00 2001 From: Romain Menke <11521496+romainmenke@users.noreply.github.com> Date: Tue, 28 Nov 2023 00:04:04 +0100 Subject: [PATCH 02/12] fix: stackoverflow in `ReadFrom` (#925) * fix stackoverflow in ReadFrom - failing test * fix stackoverflow --- http/interceptor.go | 2 +- http/interceptor_test.go | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/http/interceptor.go b/http/interceptor.go index a7b616ab5..2f3352e97 100644 --- a/http/interceptor.go +++ b/http/interceptor.go @@ -109,7 +109,7 @@ func (i *rwInterceptor) Header() http.Header { } func (i *rwInterceptor) ReadFrom(r io.Reader) (n int64, err error) { - return io.Copy(i, r) + return io.Copy(struct{ io.Writer }{i}, r) } func (i *rwInterceptor) Flush() { diff --git a/http/interceptor_test.go b/http/interceptor_test.go index e4da8e700..d232996bf 100644 --- a/http/interceptor_test.go +++ b/http/interceptor_test.go @@ -214,6 +214,11 @@ func TestReadFrom(t *testing.T) { t.Errorf("unexpected error: %v", err) } + _, err = rw.(io.ReaderFrom).ReadFrom(struct{ io.Reader }{bytes.NewBuffer([]byte("hello world"))}) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + err = responseProcessor(tx, req) if err != nil { t.Errorf("unexpected error: %v", err) From 904a49a314da8c9efa351689978180f45744e9ae Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 00:04:32 +0100 Subject: [PATCH 03/12] fix(deps): update module golang.org/x/net to v0.19.0 (#930) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 8f747bdc2..c9ffa5c58 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/mccutchen/go-httpbin/v2 v2.12.0 github.com/petar-dambovaliev/aho-corasick v0.0.0-20230725210150-fb29fc3c913e github.com/tidwall/gjson v1.17.0 - golang.org/x/net v0.18.0 + golang.org/x/net v0.19.0 golang.org/x/sync v0.5.0 rsc.io/binaryregexp v0.2.0 ) @@ -33,6 +33,6 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect golang.org/x/mod v0.8.0 // indirect - golang.org/x/sys v0.14.0 // indirect + golang.org/x/sys v0.15.0 // indirect golang.org/x/tools v0.6.0 // indirect ) diff --git a/go.sum b/go.sum index 2eaa15578..74a50a00a 100644 --- a/go.sum +++ b/go.sum @@ -34,6 +34,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= 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.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= @@ -49,6 +51,8 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= From 4aeb2408c5c18444e28fe5275abee30d1f1962e7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 7 Dec 2023 10:43:00 -0300 Subject: [PATCH 04/12] chore(deps): update actions/stale action to v9 (#933) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/close-issues.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/close-issues.yml b/.github/workflows/close-issues.yml index c4af4c2f6..ade1c5480 100644 --- a/.github/workflows/close-issues.yml +++ b/.github/workflows/close-issues.yml @@ -10,7 +10,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/stale@v8 + - uses: actions/stale@v9 with: days-before-issue-stale: 30 days-before-issue-close: 14 From 13771ff4b68ff38c22d77102180d844aa5ca9a17 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 7 Dec 2023 10:58:26 -0300 Subject: [PATCH 05/12] chore(deps): update actions/setup-go action to v5 (#932) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/fix-dependabot.yml | 2 +- .github/workflows/fuzz.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/regression.yml | 2 +- .github/workflows/tinygo.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/fix-dependabot.yml b/.github/workflows/fix-dependabot.yml index 3f0b58e1f..11f9154b7 100644 --- a/.github/workflows/fix-dependabot.yml +++ b/.github/workflows/fix-dependabot.yml @@ -11,7 +11,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: v1.19.x cache: true diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 0d85ad6f3..036aa930b 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: '>=1.19.0' - run: go run mage.go fuzz diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3fa34c03e..96eb95a6c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,7 +20,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: v1.19.x cache: true diff --git a/.github/workflows/regression.yml b/.github/workflows/regression.yml index a8c1d8323..75f934580 100644 --- a/.github/workflows/regression.yml +++ b/.github/workflows/regression.yml @@ -23,7 +23,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - name: Install Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} cache: true diff --git a/.github/workflows/tinygo.yml b/.github/workflows/tinygo.yml index 79f623f1e..709cc5b8f 100644 --- a/.github/workflows/tinygo.yml +++ b/.github/workflows/tinygo.yml @@ -26,7 +26,7 @@ jobs: uses: actions/checkout@v4 - name: Install Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} cache: true From c0b5f7ae8cc14caa2e1567abbbae27dffd70b052 Mon Sep 17 00:00:00 2001 From: Juan Pablo Tosso Date: Thu, 7 Dec 2023 16:22:53 +0100 Subject: [PATCH 06/12] docs: update README.md (#861) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update README.md --------- Co-authored-by: Felipe Zipitría <3012076+fzipi@users.noreply.github.com> --- README.md | 52 +++++++++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 6bd87a292..33d314993 100644 --- a/README.md +++ b/README.md @@ -58,32 +58,34 @@ Coraza can be used as a library for your Go program to implement a security midd package main import ( - "fmt" - "github.com/corazawaf/coraza/v3" + "fmt" + + "github.com/corazawaf/coraza/v3" ) func main() { - // First we initialize our waf and our seclang parser - waf, err := coraza.NewWAF(coraza.NewWAFConfig(). - WithDirectives(`SecRule REMOTE_ADDR "@rx .*" "id:1,phase:1,deny,status:403"`)) - // Now we parse our rules - if err != nil { - fmt.Println(err) - } - - // Then we create a transaction and assign some variables - tx := waf.NewTransaction() - defer func() { - tx.ProcessLogging() - tx.Close() - }() - tx.ProcessConnection("127.0.0.1", 8080, "127.0.0.1", 12345) - - // Finally we process the request headers phase, which may return an interruption - if it := tx.ProcessRequestHeaders(); it != nil { - fmt.Printf("Transaction was interrupted with status %d\n", it.Status) - } + // First we initialize our waf and our seclang parser + waf, err := coraza.NewWAF(coraza.NewWAFConfig(). + WithDirectives(`SecRule REMOTE_ADDR "@rx .*" "id:1,phase:1,deny,status:403"`)) + // Now we parse our rules + if err != nil { + fmt.Println(err) + } + + // Then we create a transaction and assign some variables + tx := waf.NewTransaction() + defer func() { + tx.ProcessLogging() + tx.Close() + }() + tx.ProcessConnection("127.0.0.1", 8080, "127.0.0.1", 12345) + + // Finally we process the request headers phase, which may return an interruption + if it := tx.ProcessRequestHeaders(); it != nil { + fmt.Printf("Transaction was interrupted with status %d\n", it.Status) + } } + ``` [Examples/http-server](./examples/http-server/) provides an example to practice with Coraza. @@ -132,8 +134,8 @@ Coraza only requires Go for development. You can run `mage.go` to issue developm See the list of commands -```shell -go run mage.go -l +``` +$ go run mage.go -l Targets: check runs lint and tests. coverage runs tests with coverage and race detector enabled. @@ -163,8 +165,8 @@ Our vulnerability management team will respond within 3 working days of your rep ## Thanks -* Modsecurity team for creating ModSecurity * OWASP Coreruleset team for the CRS and their help +* Ivan Ristić for creating ModSecurity ### Coraza on Twitter From 968dc71c6844524edc8ce54976523a1884d0874e Mon Sep 17 00:00:00 2001 From: Matteo Pace Date: Mon, 11 Dec 2023 23:20:59 +0100 Subject: [PATCH 07/12] fix: Disables implicit Cookies url decoding (#928) * Cookie not urldecoded * adds test * chore: adds comment * makes const private * QueryUnescape minor refactor * private doParseQuery avoiding public booleans * removes also comment, makes QueryUnescape private * go mod tidy * adds a couple of QueryUnescape test cases * godoc format * untrack go.work.sum, drops constants * fix comment --- .gitignore | 2 + go.sum | 4 - go.work.sum | 130 ------------------------- http/middleware_test.go | 3 +- internal/bodyprocessors/urlencoded.go | 4 +- internal/corazawaf/transaction.go | 5 +- internal/corazawaf/transaction_test.go | 29 ++++++ internal/url/url.go | 55 ++++++----- internal/url/url_test.go | 44 ++++++--- testing/coreruleset/go.mod | 6 +- testing/coreruleset/go.sum | 12 +-- 11 files changed, 109 insertions(+), 185 deletions(-) delete mode 100644 go.work.sum diff --git a/.gitignore b/.gitignore index 5bd131a25..09d9a69ba 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ coraza-waf __debug_bin build/ + +go.work.sum diff --git a/go.sum b/go.sum index 74a50a00a..06215a1c6 100644 --- a/go.sum +++ b/go.sum @@ -32,8 +32,6 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= -golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -49,8 +47,6 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/go.work.sum b/go.work.sum deleted file mode 100644 index 21cbdf6ac..000000000 --- a/go.work.sum +++ /dev/null @@ -1,130 +0,0 @@ -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/pelletier/go-toml v1.9.1/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= -golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/http/middleware_test.go b/http/middleware_test.go index 404dfaa2d..d1bc2cf08 100644 --- a/http/middleware_test.go +++ b/http/middleware_test.go @@ -101,8 +101,7 @@ SecRule &REQUEST_HEADERS:Transfer-Encoding "!@eq 0" "id:1,phase:1,deny" } if it == nil { t.Fatal("Expected interruption") - } - if it.RuleID != 1 { + } else if it.RuleID != 1 { t.Fatalf("Expected rule 1 to be triggered, got rule %d", it.RuleID) } if err := tx.Close(); err != nil { diff --git a/internal/bodyprocessors/urlencoded.go b/internal/bodyprocessors/urlencoded.go index 3a4c26e81..fe0176bc4 100644 --- a/internal/bodyprocessors/urlencoded.go +++ b/internal/bodyprocessors/urlencoded.go @@ -10,7 +10,7 @@ import ( "github.com/corazawaf/coraza/v3/experimental/plugins/plugintypes" "github.com/corazawaf/coraza/v3/internal/collections" - "github.com/corazawaf/coraza/v3/internal/url" + urlutil "github.com/corazawaf/coraza/v3/internal/url" ) type urlencodedBodyProcessor struct { @@ -23,7 +23,7 @@ func (*urlencodedBodyProcessor) ProcessRequest(reader io.Reader, v plugintypes.T } b := buf.String() - values := url.ParseQuery(b, '&') + values := urlutil.ParseQuery(b, '&') argsCol := v.ArgsPost() for k, vs := range values { argsCol.Set(k, vs) diff --git a/internal/corazawaf/transaction.go b/internal/corazawaf/transaction.go index 3f0be3842..da65096f3 100644 --- a/internal/corazawaf/transaction.go +++ b/internal/corazawaf/transaction.go @@ -321,7 +321,8 @@ func (tx *Transaction) AddRequestHeader(key string, value string) { } case "cookie": // Cookies use the same syntax as GET params but with semicolon (;) separator - values := urlutil.ParseQuery(value, ';') + // WithoutUnescape is used to avoid implicitly performing an URL decode on the cookies + values := urlutil.ParseQueryWithoutUnescape(value, ';') for k, vr := range values { for _, v := range vr { tx.variables.requestCookies.Add(k, v) @@ -535,7 +536,7 @@ func (tx *Transaction) GetStopWatch() string { } // GetField Retrieve data from collections applying exceptions -// In future releases we may remove de exceptions slice and +// In future releases we may remove the exceptions slice and // make it easier to use func (tx *Transaction) GetField(rv ruleVariableParams) []types.MatchData { col := tx.Collection(rv.Variable) diff --git a/internal/corazawaf/transaction_test.go b/internal/corazawaf/transaction_test.go index 938aef632..da57336ae 100644 --- a/internal/corazawaf/transaction_test.go +++ b/internal/corazawaf/transaction_test.go @@ -806,6 +806,35 @@ func TestHeaderSetters(t *testing.T) { } } +func TestCookiesNotUrldecoded(t *testing.T) { + waf := NewWAF() + tx := waf.NewTransaction() + fullCookie := "abc=%7Bd+e+f%7D;hij=%7Bklm%7D" + expectedUrlencodedAbcCookieValue := "%7Bd+e+f%7D" + unexpectedUrldencodedAbcCookieValue := "{d e f}" + tx.AddRequestHeader("cookie", fullCookie) + c := tx.variables.requestCookies.Get("abc")[0] + if c != expectedUrlencodedAbcCookieValue { + if c == unexpectedUrldencodedAbcCookieValue { + t.Errorf("failed to set cookie, unexpected urldecoding. Got: %q, expected: %q", unexpectedUrldencodedAbcCookieValue, expectedUrlencodedAbcCookieValue) + } else { + t.Errorf("failed to set cookie, got %q", c) + } + } + if tx.variables.requestHeaders.Get("cookie")[0] != fullCookie { + t.Errorf("failed to set request header, got: %q, expected: %q", tx.variables.requestHeaders.Get("cookie")[0], fullCookie) + } + if !utils.InSlice("cookie", collectionValues(t, tx.variables.requestHeadersNames)) { + t.Error("failed to set header name", collectionValues(t, tx.variables.requestHeadersNames)) + } + if !utils.InSlice("abc", collectionValues(t, tx.variables.requestCookiesNames)) { + t.Error("failed to set cookie name") + } + if err := tx.Close(); err != nil { + t.Error(err) + } +} + func collectionValues(t *testing.T, col collection.Collection) []string { t.Helper() var values []string diff --git a/internal/url/url.go b/internal/url/url.go index abc33a440..c6087d41e 100644 --- a/internal/url/url.go +++ b/internal/url/url.go @@ -9,8 +9,16 @@ import ( // ParseQuery parses the URL-encoded query string and returns the corresponding map. // It takes separators as parameter, for example: & or ; or &; -// It returns error if the query string is malformed. func ParseQuery(query string, separator byte) map[string][]string { + return doParseQuery(query, separator, true) +} + +// ParseQueryWithoutUnescape is a sibling of ParseQuery, but without performing URL unescape of keys and values. +func ParseQueryWithoutUnescape(query string, separator byte) map[string][]string { + return doParseQuery(query, separator, false) +} + +func doParseQuery(query string, separator byte, urlUnescape bool) map[string][]string { m := make(map[string][]string) for query != "" { key := query @@ -26,15 +34,17 @@ func ParseQuery(query string, separator byte) map[string][]string { if i := strings.IndexByte(key, '='); i >= 0 { key, value = key[:i], key[i+1:] } - key = QueryUnescape(key) - value = QueryUnescape(value) + if urlUnescape { + key = queryUnescape(key) + value = queryUnescape(value) + } m[key] = append(m[key], value) } return m } -// QueryUnescape is a non-strict version of net/url.QueryUnescape. -func QueryUnescape(input string) string { +// queryUnescape is a non-strict version of net/url.QueryUnescape. +func queryUnescape(input string) string { ilen := len(input) res := strings.Builder{} res.Grow(ilen) @@ -49,27 +59,13 @@ func QueryUnescape(input string) string { res.WriteByte(ci) continue } - hi := input[i+1] - lo := input[i+2] - switch { - case hi >= '0' && hi <= '9': - hi -= '0' - case hi >= 'a' && hi <= 'f': - hi -= 'a' - 10 - case hi >= 'A' && hi <= 'F': - hi -= 'A' - 10 - default: + hi, ok := hexDigitToByte(input[i+1]) + if !ok { res.WriteByte(ci) continue } - switch { - case lo >= '0' && lo <= '9': - lo -= '0' - case lo >= 'a' && lo <= 'f': - lo -= 'a' - 10 - case lo >= 'A' && lo <= 'F': - lo -= 'A' - 10 - default: + lo, ok := hexDigitToByte(input[i+2]) + if !ok { res.WriteByte(ci) continue } @@ -81,3 +77,16 @@ func QueryUnescape(input string) string { } return res.String() } + +func hexDigitToByte(digit byte) (byte, bool) { + switch { + case digit >= '0' && digit <= '9': + return digit - '0', true + case digit >= 'a' && digit <= 'f': + return digit - 'a' + 10, true + case digit >= 'A' && digit <= 'F': + return digit - 'A' + 10, true + default: + return 0, false + } +} diff --git a/internal/url/url_test.go b/internal/url/url_test.go index 72c068a4f..7194f4a53 100644 --- a/internal/url/url_test.go +++ b/internal/url/url_test.go @@ -7,27 +7,45 @@ import ( "testing" ) +var parseQueryInput = `var=EmptyValue'||(select extractvalue(xmltype('%awpsd;` + func TestUrlPayloads(t *testing.T) { - out := `var=EmptyValue'||(select extractvalue(xmltype('%awpsd;` - q := ParseQuery(out, '&') + q := ParseQuery(parseQueryInput, '&') if len(q["var"]) == 0 { t.Error("var is empty") } } -func TestQueryUnescape(t *testing.T) { - payloads := map[string]string{ - "sample": "sample", - "s%20ample": "s ample", - "s+ample": "s ample", - "s%2fample": "s/ample", - "s% ample": "s% ample", // non-strict sample - "s%ssample": "s%ssample", // non-strict sample - "s%00ample": "s\x00ample", +func BenchmarkParseQuery(b *testing.B) { + for i := 0; i < b.N; i++ { + ParseQuery(parseQueryInput, '&') } - for k, v := range payloads { - if out := QueryUnescape(k); out != v { +} + +var queryUnescapePayloads = map[string]string{ + "sample": "sample", + "s%20ample": "s ample", + "s+ample": "s ample", + "s%2fample": "s/ample", + "s% ample": "s% ample", // non-strict sample + "s%ssample": "s%ssample", // non-strict sample + "s%00ample": "s\x00ample", + "%7B%%7d": "{%}", + "%7B+%+%7d": "{ % }", +} + +func TestQueryUnescape(t *testing.T) { + for k, v := range queryUnescapePayloads { + if out := queryUnescape(k); out != v { t.Errorf("Error parsing %q, got %q and expected %q", k, out, v) } } } + +func BenchmarkQueryUnescape(b *testing.B) { + for i := 0; i < b.N; i++ { + for k := range queryUnescapePayloads { + queryUnescape(k) + } + } +} diff --git a/testing/coreruleset/go.mod b/testing/coreruleset/go.mod index 4ef4f8a16..653552182 100644 --- a/testing/coreruleset/go.mod +++ b/testing/coreruleset/go.mod @@ -35,10 +35,10 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/yargevad/filepathx v1.0.0 // indirect - golang.org/x/crypto v0.15.0 // indirect - golang.org/x/net v0.18.0 // indirect + golang.org/x/crypto v0.16.0 // indirect + golang.org/x/net v0.19.0 // indirect golang.org/x/sync v0.5.0 // indirect - golang.org/x/sys v0.14.0 // indirect + golang.org/x/sys v0.15.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/binaryregexp v0.2.0 // indirect diff --git a/testing/coreruleset/go.sum b/testing/coreruleset/go.sum index c666c7eef..a7387979b 100644 --- a/testing/coreruleset/go.sum +++ b/testing/coreruleset/go.sum @@ -314,8 +314,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= -golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -343,8 +343,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= -golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= -golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -394,8 +394,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= From 93593ff36c46aaf539d73abf8cc2d1754f4a8b37 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 09:42:20 -0300 Subject: [PATCH 08/12] chore(deps): update github/codeql-action action to v3 (#941) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b29985f6b..0648fd0ab 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -13,12 +13,12 @@ jobs: uses: actions/checkout@v4 - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: go - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From fc1596e54f7dab4e9bebeafda57911e7d58bac57 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 14:14:14 -0300 Subject: [PATCH 09/12] fix(deps): update module github.com/mccutchen/go-httpbin/v2 to v2.13.1 (#939) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index c9ffa5c58..ef518d7a4 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/corazawaf/libinjection-go v0.1.2 github.com/foxcpp/go-mockdns v1.0.0 github.com/magefile/mage v1.15.0 - github.com/mccutchen/go-httpbin/v2 v2.12.0 + github.com/mccutchen/go-httpbin/v2 v2.13.1 github.com/petar-dambovaliev/aho-corasick v0.0.0-20230725210150-fb29fc3c913e github.com/tidwall/gjson v1.17.0 golang.org/x/net v0.19.0 diff --git a/go.sum b/go.sum index 06215a1c6..ec6c6109a 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/mccutchen/go-httpbin/v2 v2.12.0 h1:MPrFw/Avug0E83SN/j5SYDuD9By0GDAJ9hNTR4RwjyU= github.com/mccutchen/go-httpbin/v2 v2.12.0/go.mod h1:f4DUXYlU6yH0V81O4lJIwqpmYdTXXmYwzxMnYEimFPk= +github.com/mccutchen/go-httpbin/v2 v2.13.1 h1:mDTz2RTD3tugs1BKZM7o6YJsXODYWNvjKZko30B/aWk= +github.com/mccutchen/go-httpbin/v2 v2.13.1/go.mod h1:f4DUXYlU6yH0V81O4lJIwqpmYdTXXmYwzxMnYEimFPk= github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= From 3b532a6df9ee403a0a7468826fba022907a33daa Mon Sep 17 00:00:00 2001 From: blotus Date: Mon, 18 Dec 2023 16:48:01 +0100 Subject: [PATCH 10/12] feat: add uppercase transformation (#935) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add uppercase transformation * add uppercase.json for tests --------- Co-authored-by: Felipe Zipitría <3012076+fzipi@users.noreply.github.com> --- .../transformations/testdata/uppercase.json | 38 ++++++++++ internal/transformations/transformations.go | 1 + internal/transformations/uppercase.go | 15 ++++ internal/transformations/uppercase_test.go | 69 +++++++++++++++++++ 4 files changed, 123 insertions(+) create mode 100644 internal/transformations/testdata/uppercase.json create mode 100644 internal/transformations/uppercase.go create mode 100644 internal/transformations/uppercase_test.go diff --git a/internal/transformations/testdata/uppercase.json b/internal/transformations/testdata/uppercase.json new file mode 100644 index 000000000..c8b8f15e9 --- /dev/null +++ b/internal/transformations/testdata/uppercase.json @@ -0,0 +1,38 @@ +[ + { + "input" : "", + "name" : "uppercase", + "type" : "tfn", + "ret" : 0, + "output" : "" + }, + { + "output" : "TESTCASE", + "ret" : 0, + "name" : "uppercase", + "input" : "testcase", + "type" : "tfn" + }, + { + "ret" : 0, + "input" : "test\u0000case", + "type" : "tfn", + "name" : "uppercase", + "output" : "TEST\u0000CASE" + }, + { + "output" : "TESTCASE", + "input" : "testcase", + "name" : "uppercase", + "type" : "tfn", + "ret" : 1 + }, + { + "ret" : 1, + "input" : "Test\u0000Case", + "name" : "uppercase", + "type" : "tfn", + "output" : "TEST\u0000CASE" + } + ] + \ No newline at end of file diff --git a/internal/transformations/transformations.go b/internal/transformations/transformations.go index bcfa533b6..75a9f58fa 100644 --- a/internal/transformations/transformations.go +++ b/internal/transformations/transformations.go @@ -51,6 +51,7 @@ func init() { Register("replaceComments", replaceComments) Register("replaceNulls", replaceNulls) Register("sha1", sha1T) + Register("uppercase", upperCase) Register("urlDecode", urlDecode) Register("urlDecodeUni", urlDecodeUni) Register("urlEncode", urlEncode) diff --git a/internal/transformations/uppercase.go b/internal/transformations/uppercase.go new file mode 100644 index 000000000..c3b6da8e7 --- /dev/null +++ b/internal/transformations/uppercase.go @@ -0,0 +1,15 @@ +// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +package transformations + +import ( + "strings" +) + +func upperCase(data string) (string, bool, error) { + // TODO: Explicit implementation of ToUpper would allow optimizing away the byte by byte comparison for returning the changed boolean + // See https://github.com/corazawaf/coraza/pull/778#discussion_r1186963422 + transformedData := strings.ToUpper(data) + return transformedData, data != transformedData, nil +} diff --git a/internal/transformations/uppercase_test.go b/internal/transformations/uppercase_test.go new file mode 100644 index 000000000..e2172e09d --- /dev/null +++ b/internal/transformations/uppercase_test.go @@ -0,0 +1,69 @@ +// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +package transformations + +import "testing" + +func TestUpperCase(t *testing.T) { + tests := []struct { + input string + want string + }{ + { + input: "TestCase", + want: "TESTCASE", + }, + { + input: "test\u0000case", + want: "TEST\u0000CASE", + }, + { + input: "TESTCASE", + want: "TESTCASE", + }, + { + input: "", + want: "", + }, + { + input: "ThIs Is A tExT fOr TeStInG uPPerCAse FuNcTiOnAlItY.", + want: "THIS IS A TEXT FOR TESTING UPPERCASE FUNCTIONALITY.", + }, + } + + for _, tc := range tests { + tt := tc + t.Run(tt.input, func(t *testing.T) { + have, changed, err := upperCase(tt.input) + if err != nil { + t.Error(err) + } + if tt.input == tt.want && changed || tt.input != tt.want && !changed { + t.Errorf("input %q, have %q with changed %t", tt.input, have, changed) + } + if have != tt.want { + t.Errorf("have %q, want %q", have, tt.want) + } + }) + } +} + +func BenchmarkUppercase(b *testing.B) { + tests := []string{ + "tesTcase", + "ThIs Is A tExT fOr TeStInG lOwErCaSe FuNcTiOnAlItY.ThIs Is A tExT fOr TeStInG lOwErCaSe FuNcTiOnAlItY. ThIs Is A tExT fOr TeStInG lOwErCaSe FuNcTiOnAlItY.ThIs Is A tExT fOr TeStInG lOwErCaSe FuNcTiOnAlItY.", + } + for i := 0; i < b.N; i++ { + for _, tt := range tests { + b.Run(tt, func(b *testing.B) { + for j := 0; j < b.N; j++ { + _, _, err := upperCase(tt) + if err != nil { + b.Error(err) + } + } + }) + } + } +} From f1cfd138d37a320463e02c831520aec9d73bc75d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Zipitr=C3=ADa?= <3012076+fzipi@users.noreply.github.com> Date: Mon, 18 Dec 2023 12:48:50 -0300 Subject: [PATCH 11/12] fix: parse multiple cookies with spaces (#943) * fix: parse multiple cookies with spaces --------- Signed-off-by: Felipe Zipitria --- go.sum | 2 - internal/cookies/cookies.go | 36 ++++++++++ internal/cookies/cookies_test.go | 97 ++++++++++++++++++++++++++ internal/corazawaf/transaction.go | 19 ++++- internal/corazawaf/transaction_test.go | 22 ++++++ internal/url/url.go | 5 -- 6 files changed, 171 insertions(+), 10 deletions(-) create mode 100644 internal/cookies/cookies.go create mode 100644 internal/cookies/cookies_test.go diff --git a/go.sum b/go.sum index ec6c6109a..a66a41595 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,6 @@ github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6 github.com/foxcpp/go-mockdns v1.0.0/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4= github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= -github.com/mccutchen/go-httpbin/v2 v2.12.0 h1:MPrFw/Avug0E83SN/j5SYDuD9By0GDAJ9hNTR4RwjyU= -github.com/mccutchen/go-httpbin/v2 v2.12.0/go.mod h1:f4DUXYlU6yH0V81O4lJIwqpmYdTXXmYwzxMnYEimFPk= github.com/mccutchen/go-httpbin/v2 v2.13.1 h1:mDTz2RTD3tugs1BKZM7o6YJsXODYWNvjKZko30B/aWk= github.com/mccutchen/go-httpbin/v2 v2.13.1/go.mod h1:f4DUXYlU6yH0V81O4lJIwqpmYdTXXmYwzxMnYEimFPk= github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= diff --git a/internal/cookies/cookies.go b/internal/cookies/cookies.go new file mode 100644 index 000000000..250e45397 --- /dev/null +++ b/internal/cookies/cookies.go @@ -0,0 +1,36 @@ +package cookies + +import ( + "net/textproto" + "strings" +) + +// ParseCookies parses cookies and splits in name, value pairs. Won't check for valid names nor values. +// If there are multiple cookies with the same name, it will append to the list with the same name key. +// Loosely based in the stdlib src/net/http/cookie.go +func ParseCookies(rawCookies string) map[string][]string { + cookies := make(map[string][]string) + + rawCookies = textproto.TrimString(rawCookies) + + if rawCookies == "" { + return cookies + } + + var part string + for len(rawCookies) > 0 { // continue since we have rest + part, rawCookies, _ = strings.Cut(rawCookies, ";") + part = textproto.TrimString(part) + if part == "" { + continue + } + name, val, _ := strings.Cut(part, "=") + name = textproto.TrimString(name) + // if name is empty (eg: "Cookie: =foo;") skip it + if name == "" { + continue + } + cookies[name] = append(cookies[name], val) + } + return cookies +} diff --git a/internal/cookies/cookies_test.go b/internal/cookies/cookies_test.go new file mode 100644 index 000000000..83ad1466c --- /dev/null +++ b/internal/cookies/cookies_test.go @@ -0,0 +1,97 @@ +package cookies + +import ( + "testing" +) + +func equalMaps(map1 map[string][]string, map2 map[string][]string) bool { + if len(map1) != len(map2) { + return false + } + + // Iterate through the key-value pairs of the first map + for key, slice1 := range map1 { + // Check if the key exists in the second map + slice2, ok := map2[key] + if !ok { + return false + } + + // Compare the values of the corresponding keys + for i, val1 := range slice1 { + val2 := slice2[i] + + // Compare the elements + if val1 != val2 { + return false + } + } + } + + return true +} + +func TestParseCookies(t *testing.T) { + type args struct { + rawCookies string + } + tests := []struct { + name string + args args + want map[string][]string + }{ + { + name: "EmptyString", + args: args{rawCookies: " "}, + want: map[string][]string{}, + }, + { + name: "SimpleCookie", + args: args{rawCookies: "test=test_value"}, + want: map[string][]string{"test": {"test_value"}}, + }, + { + name: "MultipleCookies", + args: args{rawCookies: "test1=test_value1; test2=test_value2"}, + want: map[string][]string{"test1": {"test_value1"}, "test2": {"test_value2"}}, + }, + { + name: "SpacesInCookieName", + args: args{rawCookies: " test1 =test_value1; test2 =test_value2"}, + want: map[string][]string{"test1": {"test_value1"}, "test2": {"test_value2"}}, + }, + { + name: "SpacesInCookieValue", + args: args{rawCookies: "test1=test _value1; test2 =test_value2"}, + want: map[string][]string{"test1": {"test _value1"}, "test2": {"test_value2"}}, + }, + { + name: "EmptyCookie", + args: args{rawCookies: ";;foo=bar"}, + want: map[string][]string{"foo": {"bar"}}, + }, + { + name: "EmptyName", + args: args{rawCookies: "=bar;"}, + want: map[string][]string{}, + }, + { + name: "MultipleEqualsInValues", + args: args{rawCookies: "test1=val==ue1;test2=value2"}, + want: map[string][]string{"test1": {"val==ue1"}, "test2": {"value2"}}, + }, + { + name: "RepeatedCookieNameShouldGiveList", + args: args{rawCookies: "test1=value1;test1=value2"}, + want: map[string][]string{"test1": {"value1", "value2"}}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ParseCookies(tt.args.rawCookies) + if !equalMaps(got, tt.want) { + t.Errorf("ParseCookies() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/corazawaf/transaction.go b/internal/corazawaf/transaction.go index da65096f3..720c78272 100644 --- a/internal/corazawaf/transaction.go +++ b/internal/corazawaf/transaction.go @@ -22,6 +22,7 @@ import ( "github.com/corazawaf/coraza/v3/internal/auditlog" "github.com/corazawaf/coraza/v3/internal/bodyprocessors" "github.com/corazawaf/coraza/v3/internal/collections" + "github.com/corazawaf/coraza/v3/internal/cookies" "github.com/corazawaf/coraza/v3/internal/corazarules" "github.com/corazawaf/coraza/v3/internal/corazatypes" stringsutil "github.com/corazawaf/coraza/v3/internal/strings" @@ -320,9 +321,21 @@ func (tx *Transaction) AddRequestHeader(key string, value string) { tx.variables.reqbodyProcessor.Set("MULTIPART") } case "cookie": - // Cookies use the same syntax as GET params but with semicolon (;) separator - // WithoutUnescape is used to avoid implicitly performing an URL decode on the cookies - values := urlutil.ParseQueryWithoutUnescape(value, ';') + // 4.2. Cookie + // + // 4.2.1. Syntax + // + // The user agent sends stored cookies to the origin server in the + // Cookie header. If the server conforms to the requirements in + // Section 4.1 (and the user agent conforms to the requirements in + // Section 5), the user agent will send a Cookie header that conforms to + // the following grammar: + // + // cookie-header = "Cookie:" OWS cookie-string OWS + // cookie-string = cookie-pair *( ";" SP cookie-pair ) + // + // There is no URL Decode performed no the cookies + values := cookies.ParseCookies(value) for k, vr := range values { for _, v := range vr { tx.variables.requestCookies.Add(k, v) diff --git a/internal/corazawaf/transaction_test.go b/internal/corazawaf/transaction_test.go index da57336ae..34ccb9a3e 100644 --- a/internal/corazawaf/transaction_test.go +++ b/internal/corazawaf/transaction_test.go @@ -835,6 +835,28 @@ func TestCookiesNotUrldecoded(t *testing.T) { } } +func TestMultipleCookiesWithSpaceBetweenThem(t *testing.T) { + waf := NewWAF() + tx := waf.NewTransaction() + multipleCookies := "cookie1=value1; cookie2=value2; cookie1=value2" + tx.AddRequestHeader("cookie", multipleCookies) + v11 := tx.variables.requestCookies.Get("cookie1")[0] + if v11 != "value1" { + t.Errorf("failed to set cookie, got %q", v11) + } + v12 := tx.variables.requestCookies.Get("cookie1")[1] + if v12 != "value2" { + t.Errorf("failed to set cookie, got %q", v12) + } + v2 := tx.variables.requestCookies.Get("cookie2")[0] + if v2 != "value2" { + t.Errorf("failed to set cookie, got %q", v2) + } + if err := tx.Close(); err != nil { + t.Error(err) + } +} + func collectionValues(t *testing.T, col collection.Collection) []string { t.Helper() var values []string diff --git a/internal/url/url.go b/internal/url/url.go index c6087d41e..ea6dca12f 100644 --- a/internal/url/url.go +++ b/internal/url/url.go @@ -13,11 +13,6 @@ func ParseQuery(query string, separator byte) map[string][]string { return doParseQuery(query, separator, true) } -// ParseQueryWithoutUnescape is a sibling of ParseQuery, but without performing URL unescape of keys and values. -func ParseQueryWithoutUnescape(query string, separator byte) map[string][]string { - return doParseQuery(query, separator, false) -} - func doParseQuery(query string, separator byte, urlUnescape bool) map[string][]string { m := make(map[string][]string) for query != "" { From b887a580da2c8d76e0abbf8f5e029735d0f89313 Mon Sep 17 00:00:00 2001 From: Matteo Pace Date: Wed, 3 Jan 2024 16:29:11 +0100 Subject: [PATCH 12/12] fix: more forgiving base64 transformation [custom implementation] (#944) * more forgiving base64 transformation * better comment about second DecodeString call * custom implementation * comments * address review, minor refactor * moves new line characters check * Extends tests with golang base64 tests * updates link with the permalink to the exact borrowed payloads * adds note about using custom implementation --- internal/transformations/base64decode.go | 82 ++++++++- internal/transformations/base64decode_test.go | 170 +++++++++++++++++- 2 files changed, 236 insertions(+), 16 deletions(-) diff --git a/internal/transformations/base64decode.go b/internal/transformations/base64decode.go index c4be9897c..76345599d 100644 --- a/internal/transformations/base64decode.go +++ b/internal/transformations/base64decode.go @@ -3,18 +3,82 @@ package transformations -import ( - "encoding/base64" +import "strings" - stringsutil "github.com/corazawaf/coraza/v3/internal/strings" -) +var base64DecMap = []byte{ + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 62, 127, 127, 127, 63, 52, 53, + 54, 55, 56, 57, 58, 59, 60, 61, 127, 127, + 127, 64, 127, 127, 127, 0, 1, 2, 3, 4, + 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 127, 127, 127, 127, 127, 127, 26, 27, 28, + 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, + 49, 50, 51, 127, 127, 127, 127, 127, +} // base64decode decodes a Base64-encoded string. +// Padding is optional. +// Partial decoding is returned up to the first invalid character (if any). +// New line characters (\r and \n) are ignored. +// Note: a custom base64 decoder is used in order to return partial decoding when an error arises. It +// would be possible to use the standard library only relying on undocumented behaviors of the decoder. +// For more context, see https://github.com/corazawaf/coraza/pull/940 func base64decode(data string) (string, bool, error) { - dec, err := base64.StdEncoding.DecodeString(data) - if err != nil { - // Forgiving implementation, which ignores invalid characters - return data, false, nil + res := doBase64decode(data) + return res, true, nil +} + +func doBase64decode(src string) string { + slen := len(src) + if slen == 0 { + return src + } + + var n, x int + var dst strings.Builder + dst.Grow(slen) + + for i := 0; i < slen; i++ { + currChar := src[i] + // new line characters are ignored. + if currChar == '\r' || currChar == '\n' { + continue + } + // If invalid character or padding reached, we stop decoding + if currChar == '=' || currChar == ' ' || currChar > 127 { + break + } + decodedChar := base64DecMap[currChar] + // Another condition of invalid character + if decodedChar == 127 { + break + } + + x = (x << 6) | int(decodedChar&0x3F) + n++ + if n == 4 { + dst.WriteByte(byte(x >> 16)) + dst.WriteByte(byte(x >> 8)) + dst.WriteByte(byte(x)) + n = 0 + x = 0 + } } - return stringsutil.WrapUnsafe(dec), true, nil + + // Handle any remaining characters + if n == 2 { + x <<= 12 + dst.WriteByte(byte(x >> 16)) + } else if n == 3 { + x <<= 6 + dst.WriteByte(byte(x >> 16)) + dst.WriteByte(byte(x >> 8)) + } + + return dst.String() } diff --git a/internal/transformations/base64decode_test.go b/internal/transformations/base64decode_test.go index d022b1659..f6842c07d 100644 --- a/internal/transformations/base64decode_test.go +++ b/internal/transformations/base64decode_test.go @@ -10,17 +10,173 @@ import ( "testing" ) -var b64DecodeTests = []string{ - "VGVzdENhc2U=", - "P.HNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==", - "VGVzdABDYXNl", +var b64DecodeTests = []struct { + name string + input string + expected string +}{ + { + name: "Valid", + input: "VGVzdENhc2U=", + expected: "TestCase", + }, + { + name: "Valid with \u0000", + input: "VGVzdABDYXNl", + expected: "Test\x00Case", + }, + { + name: "Valid without padding", + input: "VGVzdENhc2U", + expected: "TestCase", + }, + { + name: "Valid without longer padding", + input: "PA==", + expected: "<", + }, + { + name: "valid ", + input: "PFRFU1Q+", + expected: "", + }, + { + name: "Malformed base64 encoding", + input: "PHNjcmlwd", + expected: "