diff --git a/http/interceptor.go b/http/interceptor.go index 2f3352e97..06d765635 100644 --- a/http/interceptor.go +++ b/http/interceptor.go @@ -43,6 +43,7 @@ func (i *rwInterceptor) WriteHeader(statusCode int) { i.statusCode = statusCode if it := i.tx.ProcessResponseHeaders(statusCode, i.proto); it != nil { + i.cleanHeaders() i.Header().Set("Content-Length", "0") i.statusCode = obtainStatusCodeFromInterruptionOrDefault(it, i.statusCode) i.flushWriteHeader() @@ -65,6 +66,13 @@ func (i *rwInterceptor) flushWriteHeader() { } } +// cleanHeaders removes all headers from the response +func (i *rwInterceptor) cleanHeaders() { + for k := range i.w.Header() { + i.w.Header().Del(k) + } +} + // Write buffers the response body until the request body limit is reach or an // interruption is triggered, this buffer is later used to analyse the body in // the response processor. @@ -88,7 +96,10 @@ func (i *rwInterceptor) Write(b []byte) (int, error) { // to it, otherwise we just send it to the response writer. it, n, err := i.tx.WriteResponseBody(b) if it != nil { - i.overrideWriteHeader(it.Status) + // if there is an interruption we must clean the headers and override the status code + i.cleanHeaders() + i.Header().Set("Content-Length", "0") + i.overrideWriteHeader(obtainStatusCodeFromInterruptionOrDefault(it, i.statusCode)) // We only flush the status code after an interruption. i.flushWriteHeader() return 0, nil @@ -153,6 +164,8 @@ func wrap(w http.ResponseWriter, r *http.Request, tx types.Transaction) ( i.flushWriteHeader() return err } else if it != nil { + // if there is an interruption we must clean the headers and override the status code + i.cleanHeaders() i.Header().Set("Content-Length", "0") i.overrideWriteHeader(obtainStatusCodeFromInterruptionOrDefault(it, i.statusCode)) i.flushWriteHeader() diff --git a/http/middleware.go b/http/middleware.go index f8c4477ee..acb72e16d 100644 --- a/http/middleware.go +++ b/http/middleware.go @@ -185,6 +185,5 @@ func obtainStatusCodeFromInterruptionOrDefault(it *types.Interruption, defaultSt return statusCode } - return defaultStatusCode } diff --git a/http/middleware_test.go b/http/middleware_test.go index d1bc2cf08..249f2f661 100644 --- a/http/middleware_test.go +++ b/http/middleware_test.go @@ -238,42 +238,53 @@ type httpTest struct { respBody string expectedProto string expectedStatus int + expectedRespHeadersKeys []string expectedRespBody string } +var expectedNoBlockingHeaders = []string{"Content-Type", "Content-Length", "Coraza-Middleware", "Date"} + +// When an interruption occour, we are expecting that no response headers are sent back to the client. +var expectedBlockingHeaders = []string{"Content-Length", "Date"} + func TestHttpServer(t *testing.T) { tests := map[string]httpTest{ "no blocking": { - reqURI: "/hello", - expectedProto: "HTTP/1.1", - expectedStatus: 201, + reqURI: "/hello", + expectedProto: "HTTP/1.1", + expectedStatus: 201, + expectedRespHeadersKeys: expectedNoBlockingHeaders, }, "no blocking HTTP/2": { - http2: true, - reqURI: "/hello", - expectedProto: "HTTP/2.0", - expectedStatus: 201, + http2: true, + reqURI: "/hello", + expectedProto: "HTTP/2.0", + expectedStatus: 201, + expectedRespHeadersKeys: expectedNoBlockingHeaders, }, "args blocking": { - reqURI: "/hello?id=0", - expectedProto: "HTTP/1.1", - expectedStatus: 403, + reqURI: "/hello?id=0", + expectedProto: "HTTP/1.1", + expectedStatus: 403, + expectedRespHeadersKeys: expectedBlockingHeaders, }, "request body blocking": { - reqURI: "/hello", - reqBody: "eval('cat /etc/passwd')", - expectedProto: "HTTP/1.1", - expectedStatus: 403, + reqURI: "/hello", + reqBody: "eval('cat /etc/passwd')", + expectedProto: "HTTP/1.1", + expectedStatus: 403, + expectedRespHeadersKeys: expectedBlockingHeaders, }, "request body larger than limit (process partial)": { reqURI: "/hello", reqBody: "eval('cat /etc/passwd')", echoReqBody: true, // Coraza only sees eva, not eval - reqBodyLimit: 3, - expectedProto: "HTTP/1.1", - expectedStatus: 201, - expectedRespBody: "eval('cat /etc/passwd')", + reqBodyLimit: 3, + expectedProto: "HTTP/1.1", + expectedStatus: 201, + expectedRespHeadersKeys: expectedNoBlockingHeaders, + expectedRespBody: "eval('cat /etc/passwd')", }, "request body larger than limit (reject)": { reqURI: "/hello", @@ -283,37 +294,43 @@ func TestHttpServer(t *testing.T) { shouldRejectOnBodyLimit: true, expectedProto: "HTTP/1.1", expectedStatus: 413, + expectedRespHeadersKeys: expectedBlockingHeaders, expectedRespBody: "", }, "response headers blocking": { - reqURI: "/hello", - respHeaders: map[string]string{"foo": "bar"}, - expectedProto: "HTTP/1.1", - expectedStatus: 401, + reqURI: "/hello", + respHeaders: map[string]string{"foo": "bar"}, + expectedProto: "HTTP/1.1", + expectedStatus: 401, + expectedRespHeadersKeys: expectedBlockingHeaders, }, "response body not blocking": { - reqURI: "/hello", - respBody: "true negative response body", - expectedProto: "HTTP/1.1", - expectedStatus: 201, - expectedRespBody: "true negative response body", + reqURI: "/hello", + respBody: "true negative response body", + expectedProto: "HTTP/1.1", + expectedStatus: 201, + expectedRespHeadersKeys: expectedNoBlockingHeaders, + expectedRespBody: "true negative response body", }, "response body blocking": { - reqURI: "/hello", - respBody: "password=xxxx", - expectedProto: "HTTP/1.1", - expectedStatus: 403, - expectedRespBody: "", // blocking at response body phase means returning it empty + reqURI: "/hello", + respBody: "password=xxxx", + expectedProto: "HTTP/1.1", + expectedStatus: 403, + expectedRespBody: "", // blocking at response body phase means returning it empty + expectedRespHeadersKeys: expectedBlockingHeaders, }, "allow": { - reqURI: "/allow_me", - expectedProto: "HTTP/1.1", - expectedStatus: 201, + reqURI: "/allow_me", + expectedProto: "HTTP/1.1", + expectedStatus: 201, + expectedRespHeadersKeys: expectedNoBlockingHeaders, }, "deny passes over allow due to ordering": { - reqURI: "/allow_me?id=0", - expectedProto: "HTTP/1.1", - expectedStatus: 403, + reqURI: "/allow_me?id=0", + expectedProto: "HTTP/1.1", + expectedStatus: 403, + expectedRespHeadersKeys: expectedBlockingHeaders, }, } @@ -357,26 +374,29 @@ func TestHttpServer(t *testing.T) { func TestHttpServerWithRuleEngineOff(t *testing.T) { tests := map[string]httpTest{ "no blocking true negative": { - reqURI: "/hello", - expectedProto: "HTTP/1.1", - expectedStatus: 201, - respBody: "Hello!", - expectedRespBody: "Hello!", + reqURI: "/hello", + expectedProto: "HTTP/1.1", + expectedStatus: 201, + respBody: "Hello!", + expectedRespHeadersKeys: expectedNoBlockingHeaders, + expectedRespBody: "Hello!", }, "no blocking true positive header phase": { - reqURI: "/hello?id=0", - expectedProto: "HTTP/1.1", - expectedStatus: 201, - respBody: "Downstream works!", - expectedRespBody: "Downstream works!", + reqURI: "/hello?id=0", + expectedProto: "HTTP/1.1", + expectedStatus: 201, + respBody: "Downstream works!", + expectedRespHeadersKeys: expectedNoBlockingHeaders, + expectedRespBody: "Downstream works!", }, "no blocking true positive body phase": { - reqURI: "/hello", - reqBody: "eval('cat /etc/passwd')", - expectedProto: "HTTP/1.1", - expectedStatus: 201, - respBody: "Waf is Off!", - expectedRespBody: "Waf is Off!", + reqURI: "/hello", + reqBody: "eval('cat /etc/passwd')", + expectedProto: "HTTP/1.1", + expectedStatus: 201, + respBody: "Waf is Off!", + expectedRespHeadersKeys: expectedNoBlockingHeaders, + expectedRespBody: "Waf is Off!", }, } logger := debuglog.Default(). @@ -458,6 +478,10 @@ func runAgainstWAF(t *testing.T, tCase httpTest, waf coraza.WAF) { t.Errorf("unexpected status code, want: %d, have: %d", want, have) } + if !keysExistInMap(t, tCase.expectedRespHeadersKeys, res.Header) { + t.Errorf("unexpected response headers, expected keys: %v, headers: %v", tCase.expectedRespHeadersKeys, res.Header) + } + resBody, err := io.ReadAll(res.Body) if err != nil { t.Fatalf("unexpected error when reading the response body: %v", err) @@ -480,6 +504,19 @@ func runAgainstWAF(t *testing.T, tCase httpTest, waf coraza.WAF) { } } +func keysExistInMap(t *testing.T, keys []string, m map[string][]string) bool { + t.Helper() + if len(keys) != len(m) { + return false + } + for _, key := range keys { + if _, ok := m[key]; !ok { + return false + } + } + return true +} + func TestObtainStatusCodeFromInterruptionOrDefault(t *testing.T) { tCases := map[string]struct { interruptionCode int diff --git a/internal/corazawaf/rule_test.go b/internal/corazawaf/rule_test.go index 6683f0608..c6bcedf6e 100644 --- a/internal/corazawaf/rule_test.go +++ b/internal/corazawaf/rule_test.go @@ -65,27 +65,49 @@ func TestNoMatchEvaluate(t *testing.T) { } func TestNoMatchEvaluateBecauseOfException(t *testing.T) { - r := NewRule() - r.Msg, _ = macro.NewMacro("Message") - r.LogData, _ = macro.NewMacro("Data Message") - r.ID_ = 1 - if err := r.AddVariable(variables.ArgsGet, "", false); err != nil { - t.Error(err) - } - dummyEqOp := &dummyEqOperator{} - r.SetOperator(dummyEqOp, "@eq", "0") - action := &dummyDenyAction{} - _ = r.AddAction("dummyDeny", action) - tx := NewWAF().NewTransaction() - tx.AddGetRequestArgument("test", "0") - tx.RemoveRuleTargetByID(1, variables.ArgsGet, "test") - var matchedValues []types.MatchData - matchdata := r.doEvaluate(debuglog.Noop(), types.PhaseRequestHeaders, tx, &matchedValues, 0, tx.transformationCache) - if len(matchdata) != 0 { - t.Errorf("Expected 0 matchdata, got %d", len(matchdata)) - } - if tx.interruption != nil { - t.Errorf("Expected interruption not triggered because of RemoveRuleTargetByID") + testCases := []struct { + name string + variable variables.RuleVariable + }{ + { + name: "Test ArgsGet target exception", + variable: variables.ArgsGet, + }, + { + name: "Test Args target exception", + variable: variables.Args, + }, + { + name: "Test ArgsNames target exception", + variable: variables.ArgsNames, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + r := NewRule() + r.Msg, _ = macro.NewMacro("Message") + r.LogData, _ = macro.NewMacro("Data Message") + r.ID_ = 1 + if err := r.AddVariable(tc.variable, "", false); err != nil { + t.Error(err) + } + dummyEqOp := &dummyEqOperator{} + r.SetOperator(dummyEqOp, "@eq", "0") + action := &dummyDenyAction{} + _ = r.AddAction("dummyDeny", action) + tx := NewWAF().NewTransaction() + tx.AddGetRequestArgument("test", "0") + tx.RemoveRuleTargetByID(1, tc.variable, "test") + var matchedValues []types.MatchData + matchdata := r.doEvaluate(debuglog.Noop(), types.PhaseRequestHeaders, tx, &matchedValues, 0, tx.transformationCache) + if len(matchdata) != 0 { + t.Errorf("Expected 0 matchdata, got %d", len(matchdata)) + } + if tx.interruption != nil { + t.Errorf("Expected interruption not triggered because of RemoveRuleTargetByID") + } + }) } } diff --git a/internal/corazawaf/transaction.go b/internal/corazawaf/transaction.go index 3794310b3..9f0922007 100644 --- a/internal/corazawaf/transaction.go +++ b/internal/corazawaf/transaction.go @@ -12,6 +12,7 @@ import ( "math" "mime" "net/url" + "os" "path/filepath" "strconv" "strings" @@ -26,6 +27,7 @@ import ( "github.com/corazawaf/coraza/v3/internal/cookies" "github.com/corazawaf/coraza/v3/internal/corazarules" "github.com/corazawaf/coraza/v3/internal/corazatypes" + "github.com/corazawaf/coraza/v3/internal/environment" stringsutil "github.com/corazawaf/coraza/v3/internal/strings" urlutil "github.com/corazawaf/coraza/v3/internal/url" "github.com/corazawaf/coraza/v3/types" @@ -620,6 +622,23 @@ func (tx *Transaction) RemoveRuleTargetByID(id int, variable variables.RuleVaria Variable: variable, KeyStr: key, } + + if multiphaseEvaluation && (variable == variables.Args || variable == variables.ArgsNames) { + // ARGS and ARGS_NAMES have to be splitted into _GET and _POST + switch variable { + case variables.Args: + c.Variable = variables.ArgsGet + tx.ruleRemoveTargetByID[id] = append(tx.ruleRemoveTargetByID[id], c) + c.Variable = variables.ArgsPost + tx.ruleRemoveTargetByID[id] = append(tx.ruleRemoveTargetByID[id], c) + case variables.ArgsNames: + c.Variable = variables.ArgsGetNames + tx.ruleRemoveTargetByID[id] = append(tx.ruleRemoveTargetByID[id], c) + c.Variable = variables.ArgsPostNames + tx.ruleRemoveTargetByID[id] = append(tx.ruleRemoveTargetByID[id], c) + } + return + } tx.ruleRemoveTargetByID[id] = append(tx.ruleRemoveTargetByID[id], c) } @@ -1472,13 +1491,25 @@ func (tx *Transaction) AuditLog() *auditlog.Log { // It also allows caches the transaction back into the sync.Pool func (tx *Transaction) Close() error { defer tx.WAF.txPool.Put(tx) - tx.variables.reset() + var errs []error + if environment.HasAccessToFS { + // TODO(jcchavezs): filesTmpNames should probably be a new kind of collection that + // is aware of the files and then attempt to delete them when the collection + // is resetted or an item is removed. + for _, file := range tx.variables.filesTmpNames.Get("") { + if err := os.Remove(file); err != nil { + errs = append(errs, fmt.Errorf("removing temporary file: %v", err)) + } + } + } + + tx.variables.reset() if err := tx.requestBodyBuffer.Reset(); err != nil { - errs = append(errs, err) + errs = append(errs, fmt.Errorf("reseting request body buffer: %v", err)) } if err := tx.responseBodyBuffer.Reset(); err != nil { - errs = append(errs, err) + errs = append(errs, fmt.Errorf("reseting response body buffer: %v", err)) } if tx.IsInterrupted() { @@ -1493,14 +1524,11 @@ func (tx *Transaction) Close() error { Msg("Transaction finished") } - switch { - case len(errs) == 0: + if len(errs) == 0 { return nil - case len(errs) == 1: - return fmt.Errorf("transaction close failed: %s", errs[0].Error()) - default: - return fmt.Errorf("transaction close failed:\n- %s\n- %s", errs[0].Error(), errs[1].Error()) } + + return fmt.Errorf("transaction close failed: %v", errors.Join(errs...)) } // String will return a string with the transaction debug information diff --git a/internal/corazawaf/transaction_test.go b/internal/corazawaf/transaction_test.go index b1db6a86a..1ef7fefc8 100644 --- a/internal/corazawaf/transaction_test.go +++ b/internal/corazawaf/transaction_test.go @@ -98,7 +98,7 @@ func TestTxMultipart(t *testing.T) { tx.RequestBodyLimit = 9999999 _, err := tx.ParseRequestReader(strings.NewReader(data)) if err != nil { - t.Error("Failed to parse multipart request: " + err.Error()) + t.Fatal("Failed to parse multipart request: " + err.Error()) } exp := map[string]string{ "%{args_post.text}": "test-value", @@ -108,6 +108,10 @@ func TestTxMultipart(t *testing.T) { } validateMacroExpansion(exp, tx, t) + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } } func TestTxResponse(t *testing.T) { @@ -225,7 +229,7 @@ func TestWriteRequestBody(t *testing.T) { for _, c := range chunks { if it, _, err = writeRequestBody(tx, c); err != nil { - t.Errorf("Failed to write body buffer: %s", err.Error()) + t.Fatalf("Failed to write body buffer: %s", err.Error()) } } @@ -245,11 +249,13 @@ func TestWriteRequestBody(t *testing.T) { val := tx.variables.argsPost.Get("some") if len(val) != 1 || val[0] != "result" { - t.Errorf("Failed to set urlencoded POST data with arguments: \"%s\"", strings.Join(val, "\", \"")) + t.Fatalf("Failed to set urlencoded POST data with arguments: \"%s\"", strings.Join(val, "\", \"")) } } - _ = tx.Close() + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } }) } @@ -306,7 +312,9 @@ func TestWriteRequestBodyOnLimitReached(t *testing.T) { t.Fatalf("unexpected number of bytes written") } - _ = tx.Close() + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } }) } }) @@ -353,7 +361,9 @@ func TestWriteRequestBodyIsNopWhenBodyIsNotAccesible(t *testing.T) { t.Fatalf("unexpected number of bytes written") } - _ = tx.Close() + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } }) } }) @@ -364,12 +374,12 @@ func TestResponseHeader(t *testing.T) { tx := makeTransaction(t) tx.AddResponseHeader("content-type", "test") if tx.variables.responseContentType.Get() != "test" { - t.Error("invalid RESPONSE_CONTENT_TYPE after response headers") + t.Fatal("invalid RESPONSE_CONTENT_TYPE after response headers") } interruption := tx.ProcessResponseHeaders(200, "OK") if interruption != nil { - t.Error("unexpected interruption") + t.Fatal("unexpected interruption") } } @@ -378,12 +388,16 @@ func TestProcessRequestHeadersDoesNoEvaluationOnEngineOff(t *testing.T) { tx.RuleEngine = types.RuleEngineOff if !tx.IsRuleEngineOff() { - t.Error("expected Engine off") + t.Fatal("expected Engine off") } _ = tx.ProcessRequestHeaders() if tx.lastPhase != 0 { // 0 means no phases have been evaluated - t.Error("unexpected rule evaluation") + t.Fatal("unexpected rule evaluation") + } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -391,10 +405,13 @@ func TestProcessRequestBodyDoesNoEvaluationOnEngineOff(t *testing.T) { tx := NewWAF().NewTransaction() tx.RuleEngine = types.RuleEngineOff if _, err := tx.ProcessRequestBody(); err != nil { - t.Error("failed to process request body") + t.Fatal("failed to process request body") } if tx.lastPhase != 0 { - t.Error("unexpected rule evaluation") + t.Fatal("unexpected rule evaluation") + } + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -403,7 +420,7 @@ func TestProcessResponseHeadersDoesNoEvaluationOnEngineOff(t *testing.T) { tx.RuleEngine = types.RuleEngineOff _ = tx.ProcessResponseHeaders(200, "OK") if tx.lastPhase != 0 { - t.Error("unexpected rule evaluation") + t.Fatal("unexpected rule evaluation") } } @@ -411,10 +428,10 @@ func TestProcessResponseBodyDoesNoEvaluationOnEngineOff(t *testing.T) { tx := NewWAF().NewTransaction() tx.RuleEngine = types.RuleEngineOff if _, err := tx.ProcessResponseBody(); err != nil { - t.Error("Failed to process response body") + t.Fatal("Failed to process response body") } if tx.lastPhase != 0 { - t.Error("unexpected rule evaluation") + t.Fatal("unexpected rule evaluation") } } @@ -423,7 +440,10 @@ func TestProcessLoggingDoesNoEvaluationOnEngineOff(t *testing.T) { tx.RuleEngine = types.RuleEngineOff tx.ProcessLogging() if tx.lastPhase != 0 { - t.Error("unexpected rule evaluation") + t.Fatal("unexpected rule evaluation") + } + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -432,11 +452,11 @@ func TestAuditLog(t *testing.T) { tx.AuditLogParts = types.AuditLogParts("ABCDEFGHIJK") al := tx.AuditLog() if al.Transaction().ID() != tx.id { - t.Error("invalid auditlog id") + t.Fatal("invalid auditlog id") } // TODO more checks if err := tx.Close(); err != nil { - t.Error(err) + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -525,7 +545,7 @@ func TestWriteResponseBody(t *testing.T) { for _, c := range chunks { if it, _, err = writeResponseBody(tx, c); err != nil { - t.Errorf("Failed to write body buffer: %s", err.Error()) + t.Fatalf("Failed to write body buffer: %s", err.Error()) } } @@ -545,11 +565,13 @@ func TestWriteResponseBody(t *testing.T) { // checking if the body has been populated up to the first POST arg index := strings.Index(urlencodedBody, "&") if tx.variables.responseBody.Get()[:index] != urlencodedBody[:index] { - t.Error("failed to set response body") + t.Fatal("failed to set response body") } } - _ = tx.Close() + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } }) } @@ -606,7 +628,9 @@ func TestWriteResponseBodyOnLimitReached(t *testing.T) { t.Fatalf("unexpected number of bytes written") } - _ = tx.Close() + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } }) } }) @@ -653,7 +677,9 @@ func TestWriteResponseBodyIsNopWhenBodyIsNotAccesible(t *testing.T) { t.Fatalf("unexpected number of bytes written") } - _ = tx.Close() + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } }) } }) @@ -674,21 +700,21 @@ func TestAuditLogFields(t *testing.T) { }, }) if len(tx.matchedRules) == 0 || tx.matchedRules[0].Rule().ID() != rule.ID_ { - t.Error("failed to match rule for audit") + t.Fatal("failed to match rule for audit") } al := tx.AuditLog() if len(al.Messages()) == 0 || al.Messages()[0].Data().ID() != rule.ID_ { - t.Error("failed to add rules to audit logs") + t.Fatal("failed to add rules to audit logs") } if len(al.Transaction().Request().Headers()) == 0 || al.Transaction().Request().Headers()["test"][0] != "test" { - t.Error("failed to add request header to audit log") + t.Fatal("failed to add request header to audit log") } if len(al.Transaction().Response().Headers()) == 0 || al.Transaction().Response().Headers()["test"][0] != "test" { - t.Error("failed to add Response header to audit log") + t.Fatal("failed to add Response header to audit log") } if err := tx.Close(); err != nil { - t.Error(err) + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -697,14 +723,14 @@ func TestResetCapture(t *testing.T) { tx.Capture = true tx.CaptureField(5, "test") if tx.variables.tx.Get("5")[0] != "test" { - t.Error("failed to set capture field from tx") + t.Fatal("failed to set capture field from tx") } tx.resetCaptures() if tx.variables.tx.Get("5")[0] != "" { - t.Error("failed to reset capture field from tx") + t.Fatal("failed to reset capture field from tx") } if err := tx.Close(); err != nil { - t.Error(err) + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -827,13 +853,13 @@ func TestLogCallback(t *testing.T) { } if buffer == "" || !strings.Contains(buffer, tx.id) { - t.Error("failed to call error log callback") + t.Fatal("failed to call error log callback") } if !strings.Contains(buffer, testCase.expectedLogLine) { - t.Errorf("Expected string \"%s\" with disruptive rule, got %s", testCase.expectedLogLine, buffer) + t.Fatalf("Expected string \"%s\" with disruptive rule, got %s", testCase.expectedLogLine, buffer) if err := tx.Close(); err != nil { - t.Error(err) + t.Fatal(err) } } }) @@ -847,19 +873,19 @@ func TestHeaderSetters(t *testing.T) { tx.AddRequestHeader("test1", "test2") c := tx.variables.requestCookies.Get("abc")[0] if c != "def" { - t.Errorf("failed to set cookie, got %q", c) + t.Fatalf("failed to set cookie, got %q", c) } if tx.variables.requestHeaders.Get("cookie")[0] != "abc=def;hij=klm" { - t.Error("failed to set request header") + t.Fatal("failed to set request header") } if !utils.InSlice("cookie", collectionValues(t, tx.variables.requestHeadersNames)) { - t.Error("failed to set header name", collectionValues(t, tx.variables.requestHeadersNames)) + t.Fatal("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") + t.Fatal("failed to set cookie name") } if err := tx.Close(); err != nil { - t.Error(err) + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -933,16 +959,16 @@ func TestRequestBodyProcessingAlgorithm(t *testing.T) { tx.AddRequestHeader("content-length", "7") tx.ProcessRequestHeaders() if _, err := tx.requestBodyBuffer.Write([]byte("test123")); err != nil { - t.Error("Failed to write request body buffer") + t.Fatal("Failed to write request body buffer") } if _, err := tx.ProcessRequestBody(); err != nil { - t.Error("failed to process request body") + t.Fatal("failed to process request body") } if tx.variables.requestBody.Get() != "test123" { - t.Error("failed to set request body") + t.Fatal("failed to set request body") } if err := tx.Close(); err != nil { - t.Error(err) + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -984,7 +1010,7 @@ func TestProcessBodiesSkippedIfHeadersPhasesNotReached(t *testing.T) { t.Fatalf("unexpected message, want %q, have %q", want, have) } if err := tx.Close(); err != nil { - t.Error(err) + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -996,31 +1022,31 @@ func TestTxVariables(t *testing.T) { KeyRx: regexp.MustCompile("ho.*"), } if len(tx.GetField(rv)) != 1 || tx.GetField(rv)[0].Value() != "www.test.com:80" { - t.Errorf("failed to match rule variable REQUEST_HEADERS:host, %d matches, %v", len(tx.GetField(rv)), tx.GetField(rv)) + t.Fatalf("failed to match rule variable REQUEST_HEADERS:host, %d matches, %v", len(tx.GetField(rv)), tx.GetField(rv)) } rv.Count = true if len(tx.GetField(rv)) == 0 || tx.GetField(rv)[0].Value() != "1" { - t.Errorf("failed to get count for regexp variable") + t.Fatalf("failed to get count for regexp variable") } // now nil key rv.KeyRx = nil if len(tx.GetField(rv)) == 0 { - t.Error("failed to match rule variable REQUEST_HEADERS with nil key") + t.Fatal("failed to match rule variable REQUEST_HEADERS with nil key") } rv.KeyStr = "" f := tx.GetField(rv) if len(f) == 0 { - t.Error("failed to count variable REQUEST_HEADERS ") + t.Fatal("failed to count variable REQUEST_HEADERS ") } count, err := strconv.Atoi(f[0].Value()) if err != nil { - t.Error(err) + t.Fatal(err) } if count != 5 { - t.Errorf("failed to match rule variable REQUEST_HEADERS with count, %v", rv) + t.Fatalf("failed to match rule variable REQUEST_HEADERS with count, %v", rv) } if err := tx.Close(); err != nil { - t.Error(err) + t.Fatal(err) } } @@ -1036,12 +1062,12 @@ func TestTxVariablesExceptions(t *testing.T) { } fields := tx.GetField(rv) if len(fields) != 0 { - t.Errorf("REQUEST_HEADERS:host should not match, got %d matches, %v", len(fields), fields) + t.Fatalf("REQUEST_HEADERS:host should not match, got %d matches, %v", len(fields), fields) } rv.Exceptions = nil fields = tx.GetField(rv) if len(fields) != 1 || fields[0].Value() != "www.test.com:80" { - t.Errorf("failed to match rule variable REQUEST_HEADERS:host, %d matches, %v", len(fields), fields) + t.Fatalf("failed to match rule variable REQUEST_HEADERS:host, %d matches, %v", len(fields), fields) } rv.Exceptions = []ruleVariableException{ { @@ -1050,10 +1076,10 @@ func TestTxVariablesExceptions(t *testing.T) { } fields = tx.GetField(rv) if len(fields) != 0 { - t.Errorf("REQUEST_HEADERS:host should not match, got %d matches, %v", len(fields), fields) + t.Fatalf("REQUEST_HEADERS:host should not match, got %d matches, %v", len(fields), fields) } if err := tx.Close(); err != nil { - t.Error(err) + t.Fatal(err) } } @@ -1067,11 +1093,11 @@ func TestTransactionSyncPool(t *testing.T) { }) for i := 0; i < 1000; i++ { if err := tx.Close(); err != nil { - t.Error(err) + t.Fatal(err) } tx = waf.NewTransaction() if len(tx.matchedRules) != 0 { - t.Errorf("failed to sync transaction pool, %d rules found after %d attempts", len(tx.matchedRules), i+1) + t.Fatalf("failed to sync transaction pool, %d rules found after %d attempts", len(tx.matchedRules), i+1) return } } @@ -1089,16 +1115,16 @@ func TestTxPhase4Magic(t *testing.T) { _, _ = tx.ProcessRequestBody() tx.ProcessResponseHeaders(200, "HTTP/1.1") if it, _, err := tx.WriteResponseBody([]byte("more bytes")); it != nil || err != nil { - t.Error(err) + t.Fatal(err) } if _, err := tx.ProcessResponseBody(); err != nil { - t.Error(err) + t.Fatal(err) } if tx.variables.outboundDataError.Get() != "1" { - t.Error("failed to set outbound data error") + t.Fatal("failed to set outbound data error") } if tx.variables.responseBody.Get() != "mor" { - t.Error("failed to set response body") + t.Fatal("failed to set response body") } } @@ -1117,12 +1143,16 @@ func TestVariablesMatch(t *testing.T) { for k, v := range expect { if m := (tx.Collection(k)).(*collections.Single).Get(); m != v { - t.Errorf("failed to match variable %s, Expected: %s, got: %s", k.Name(), v, m) + t.Fatalf("failed to match variable %s, Expected: %s, got: %s", k.Name(), v, m) } } if len(tx.variables.matchedVars.Get("ARGS_NAMES:sample")) == 0 { - t.Errorf("failed to match variable %s, got 0", variables.MatchedVars.Name()) + t.Fatalf("failed to match variable %s, got 0", variables.MatchedVars.Name()) + } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -1133,13 +1163,17 @@ func TestTxReqBodyForce(t *testing.T) { tx.RequestBodyAccess = true tx.ForceRequestBodyVariable = true if _, err := tx.requestBodyBuffer.Write([]byte("test")); err != nil { - t.Error(err) + t.Fatal(err) } if _, err := tx.ProcessRequestBody(); err != nil { - t.Error(err) + t.Fatal(err) } if tx.variables.requestBody.Get() != "test" { - t.Error("failed to set request body") + t.Fatal("failed to set request body") + } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -1149,13 +1183,17 @@ func TestTxReqBodyForceNegative(t *testing.T) { tx.RequestBodyAccess = true tx.ForceRequestBodyVariable = false if _, err := tx.requestBodyBuffer.Write([]byte("test")); err != nil { - t.Error(err) + t.Fatal(err) } if _, err := tx.ProcessRequestBody(); err != nil { - t.Error(err) + t.Fatal(err) } if tx.variables.requestBody.Get() == "test" { - t.Error("reqbody should not be there") + t.Fatal("reqbody should not be there") + } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -1164,10 +1202,14 @@ func TestTxProcessConnection(t *testing.T) { tx := waf.NewTransaction() tx.ProcessConnection("127.0.0.1", 80, "127.0.0.2", 8080) if tx.variables.remoteAddr.Get() != "127.0.0.1" { - t.Error("failed to set client ip") + t.Fatal("failed to set client ip") } if rp, _ := strconv.Atoi(tx.variables.remotePort.Get()); rp != 80 { - t.Error("failed to set client port") + t.Fatal("failed to set client port") + } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -1182,7 +1224,7 @@ func TestTxSetServerName(t *testing.T) { tx.lastPhase = types.PhaseRequestHeaders tx.SetServerName("coraza.io") if tx.variables.serverName.Get() != "coraza.io" { - t.Error("failed to set server name") + t.Fatal("failed to set server name") } logEntries := strings.Split(strings.TrimSpace(logBuffer.String()), "\n") if want, have := 1, len(logEntries); want != have { @@ -1192,6 +1234,10 @@ func TestTxSetServerName(t *testing.T) { if want, have := "SetServerName has been called after ProcessRequestHeaders", logEntries[0]; !strings.Contains(have, want) { t.Fatalf("unexpected message, want %q, have %q", want, have) } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } } func TestTxAddArgument(t *testing.T) { @@ -1200,15 +1246,19 @@ func TestTxAddArgument(t *testing.T) { tx.ProcessConnection("127.0.0.1", 80, "127.0.0.2", 8080) tx.AddGetRequestArgument("test", "testvalue") if tx.variables.argsGet.Get("test")[0] != "testvalue" { - t.Error("failed to set args get") + t.Fatal("failed to set args get") } tx.AddPostRequestArgument("ptest", "ptestvalue") if tx.variables.argsPost.Get("ptest")[0] != "ptestvalue" { - t.Error("failed to set args post") + t.Fatal("failed to set args post") } tx.AddPathRequestArgument("ptest2", "ptestvalue") if tx.variables.argsPath.Get("ptest2")[0] != "ptestvalue" { - t.Error("failed to set args post") + t.Fatal("failed to set args post") + } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -1218,7 +1268,11 @@ func TestTxGetField(t *testing.T) { Variable: variables.Args, } if f := tx.GetField(rvp); len(f) != 3 { - t.Errorf("failed to get field, expected 2, got %d", len(f)) + t.Fatalf("failed to get field, expected 2, got %d", len(f)) + } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -1228,19 +1282,23 @@ func TestTxProcessURI(t *testing.T) { uri := "http://example.com/path/to/file.html?query=string&other=value" tx.ProcessURI(uri, "GET", "HTTP/1.1") if s := tx.variables.requestURI.Get(); s != uri { - t.Errorf("failed to set request uri, got %s", s) + t.Fatalf("failed to set request uri, got %s", s) } if s := tx.variables.requestBasename.Get(); s != "file.html" { - t.Errorf("failed to set request path, got %s", s) + t.Fatalf("failed to set request path, got %s", s) } if tx.variables.queryString.Get() != "query=string&other=value" { - t.Error("failed to set request query") + t.Fatal("failed to set request query") } if v := tx.variables.args.FindAll(); len(v) != 2 { - t.Errorf("failed to set request args, got %d", len(v)) + t.Fatalf("failed to set request args, got %d", len(v)) } if v := tx.variables.args.FindString("other"); v[0].Value() != "value" { - t.Errorf("failed to set request args, got %v", v) + t.Fatalf("failed to set request args, got %v", v) + } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -1316,7 +1374,7 @@ func validateMacroExpansion(tests map[string]string, tx *Transaction, t *testing for k, v := range tests { m, err := macro.NewMacro(k) if err != nil { - t.Error(err) + t.Fatal(err) } res := m.Expand(tx) if res != v { @@ -1324,7 +1382,7 @@ func validateMacroExpansion(tests map[string]string, tx *Transaction, t *testing fmt.Println(tx) fmt.Println("===STACK===\n", string(debug.Stack())+"\n===STACK===") } - t.Error("Failed set transaction for " + k + ", expected " + v + ", got " + res) + t.Fatal("Failed set transaction for " + k + ", expected " + v + ", got " + res) } } } @@ -1334,28 +1392,32 @@ func TestMacro(t *testing.T) { tx.variables.tx.Set("some", []string{"secretly"}) m, err := macro.NewMacro("%{unique_id}") if err != nil { - t.Error(err) + t.Fatal(err) } if m.Expand(tx) != tx.id { - t.Errorf("%s != %s", m.Expand(tx), tx.id) + t.Fatalf("%s != %s", m.Expand(tx), tx.id) } m, err = macro.NewMacro("some complex text %{tx.some} wrapped in m") if err != nil { - t.Error(err) + t.Fatal(err) } if m.Expand(tx) != "some complex text secretly wrapped in m" { - t.Errorf("failed to expand m, got %s\n%v", m.Expand(tx), m) + t.Fatalf("failed to expand m, got %s\n%v", m.Expand(tx), m) } _, err = macro.NewMacro("some complex text %{tx.some} wrapped in m %{tx.some}") if err != nil { - t.Error(err) + t.Fatal(err) return } // TODO(anuraaga): Decouple this test from transaction implementation. // if !macro.IsExpandable() || len(macro.tokens) != 4 || macro.Expand(tx) != "some complex text secretly wrapped in m secretly" { - // t.Errorf("failed to parse replacements %v", macro.tokens) + // t.Fatalf("failed to parse replacements %v", macro.tokens) // } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } } func BenchmarkMacro(b *testing.B) { @@ -1444,6 +1506,10 @@ func TestProcessorsIdempotencyWithAlreadyRaisedInterruption(t *testing.T) { } }) } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } } func TestIterationStops(t *testing.T) { @@ -1469,16 +1535,20 @@ func TestIterationStops(t *testing.T) { }) if want, have := i+1, len(haveVars); want != have { - t.Errorf("stopped with unexpected number of variables, want %d, have %d", want, have) + t.Fatalf("stopped with unexpected number of variables, want %d, have %d", want, have) } for j, v := range haveVars { if want, have := allVars[j], v; want != have { - t.Errorf("unexpected variable at index %d, want %s, have %s", j, want.Name(), have.Name()) + t.Fatalf("unexpected variable at index %d, want %s, have %s", j, want.Name(), have.Name()) } } }) } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } } func TestTxAddResponseArgs(t *testing.T) { @@ -1486,7 +1556,7 @@ func TestTxAddResponseArgs(t *testing.T) { tx := waf.NewTransaction() tx.AddResponseArgument("samplekey", "samplevalue") if tx.variables.responseArgs.Get("samplekey")[0] != "samplevalue" { - t.Errorf("failed to add response argument") + t.Fatalf("failed to add response argument") } } @@ -1503,6 +1573,10 @@ func TestAddGetArgsWithOverlimit(t *testing.T) { if tx.variables.argsGet.Len() > waf.ArgumentLimit { t.Fatal("Argument limit is failed while add get args") } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } } } @@ -1519,6 +1593,10 @@ func TestAddPostArgsWithOverlimit(t *testing.T) { if tx.variables.argsPost.Len() > waf.ArgumentLimit { t.Fatal("Argument limit is failed while add post args") } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } } } @@ -1535,6 +1613,10 @@ func TestAddPathArgsWithOverlimit(t *testing.T) { if tx.variables.argsPath.Len() > waf.ArgumentLimit { t.Fatal("Argument limit is failed while add path args") } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } } } @@ -1551,6 +1633,10 @@ func TestAddResponseArgsWithOverlimit(t *testing.T) { if tx.variables.responseArgs.Len() > waf.ArgumentLimit { t.Fatal("Argument limit is failed while add response args") } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } } } @@ -1575,6 +1661,10 @@ func TestResponseBodyForceProcessing(t *testing.T) { if len(f) == 0 { t.Fatal("json.key not found") } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } } func TestForceRequestBodyOverride(t *testing.T) { @@ -1585,24 +1675,43 @@ func TestForceRequestBodyOverride(t *testing.T) { tx.variables.RequestBodyProcessor().(*collections.Single).Set("JSON") tx.ProcessRequestHeaders() if _, _, err := tx.WriteRequestBody([]byte("foo=bar&baz=qux")); err != nil { - t.Errorf("Failed to write request body: %v", err) + t.Fatalf("Failed to write request body: %v", err) } if _, err := tx.ProcessRequestBody(); err != nil { - t.Errorf("Failed to process request body: %v", err) + t.Fatalf("Failed to process request body: %v", err) } if tx.variables.RequestBodyProcessor().Get() != "JSON" { - t.Errorf("Failed to force request body variable") + t.Fatalf("Failed to force request body variable") } tx = waf.NewTransaction() tx.ForceRequestBodyVariable = true tx.ProcessRequestHeaders() if _, _, err := tx.WriteRequestBody([]byte("foo=bar&baz=qux")); err != nil { - t.Errorf("Failed to write request body: %v", err) + t.Fatalf("Failed to write request body: %v", err) } if _, err := tx.ProcessRequestBody(); err != nil { - t.Errorf("Failed to process request body: %v", err) + t.Fatalf("Failed to process request body: %v", err) } if tx.variables.RequestBodyProcessor().Get() != "URLENCODED" { - t.Errorf("Failed to force request body variable, got RBP: %q", tx.variables.RequestBodyProcessor().Get()) + t.Fatalf("Failed to force request body variable, got RBP: %q", tx.variables.RequestBodyProcessor().Get()) + } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } +} + +func TestCloseFails(t *testing.T) { + waf := NewWAF() + tx := waf.NewTransaction() + col := tx.Variables().FilesTmpNames().(*collections.Map) + col.Add("", "unexisting") + err := tx.Close() + if err == nil { + t.Fatalf("expected error when closing transaction") + } + + if !strings.Contains(err.Error(), "removing temporary file") { + t.Fatalf("unexpected error message: %s", err.Error()) } } diff --git a/testing/coreruleset/.ftw.yml b/testing/coreruleset/.ftw.yml index 6af95b80a..a84af7b09 100644 --- a/testing/coreruleset/.ftw.yml +++ b/testing/coreruleset/.ftw.yml @@ -7,5 +7,6 @@ testoverride: 920270-4: 'Rule works, log contains 920270. Test expects status 400 (Apache behaviour)' 920272-5: 'Rule works, log contains 920272. Test expects status 400 (Apache behaviour)' 920290-1: 'Rule works, log contains 920290. Test expects status 400 (Apache behaviour)' - 920430-8: 'Go/http does no allow HTTP/3.0 - 505 HTTP Version Not Supported' + 920290-4: 'Go/http returns 400 Bad Request: missing required Host header' + 920430-8: 'Go/http does not allow HTTP/3.0 - 505 HTTP Version Not Supported' 932200-13: 'wip' diff --git a/testing/coreruleset/coreruleset_test.go b/testing/coreruleset/coreruleset_test.go index dba5356a1..f926bda2c 100644 --- a/testing/coreruleset/coreruleset_test.go +++ b/testing/coreruleset/coreruleset_test.go @@ -29,8 +29,8 @@ import ( "github.com/coreruleset/go-ftw/test" "github.com/rs/zerolog" - coreruleset "github.com/corazawaf/coraza-coreruleset" - crstests "github.com/corazawaf/coraza-coreruleset/tests" + coreruleset "github.com/corazawaf/coraza-coreruleset/v4" + crstests "github.com/corazawaf/coraza-coreruleset/v4/tests" "github.com/corazawaf/coraza/v3" txhttp "github.com/corazawaf/coraza/v3/http" "github.com/corazawaf/coraza/v3/types" @@ -234,11 +234,16 @@ SecRule REQUEST_HEADERS:X-CRS-Test "@rx ^.*$" \ } urldecodedBody, err := url.QueryUnescape(string(body)) if err != nil { - t.Fatalf("handler can not unescape urlencoded request body: %v", err) + t.Logf("[warning] handler can not unescape urlencoded request body: %v", err) + // If the body can't be unescaped, we will keep going with the received body + urldecodedBody = string(body) } - fmt.Fprintf(w, urldecodedBody) + fmt.Fprint(w, urldecodedBody) } else { _, err = w.Write(body) + if err != nil { + t.Fatalf("handler can not write request body: %v", err) + } } case strings.HasPrefix(r.URL.Path, "/base64/"): @@ -247,10 +252,10 @@ SecRule REQUEST_HEADERS:X-CRS-Test "@rx ^.*$" \ if err != nil { t.Fatalf("handler can not decode base64: %v", err) } - fmt.Fprintf(w, string(b64Decoded)) + fmt.Fprint(w, string(b64Decoded)) default: // Common path "/status/200" defaults here - fmt.Fprintf(w, "Hello!") + fmt.Fprint(w, "Hello!") } }))) defer s.Close() diff --git a/testing/coreruleset/go.mod b/testing/coreruleset/go.mod index 016be2901..575d59290 100644 --- a/testing/coreruleset/go.mod +++ b/testing/coreruleset/go.mod @@ -4,7 +4,7 @@ go 1.20 require ( github.com/bmatcuk/doublestar/v4 v4.6.1 - github.com/corazawaf/coraza-coreruleset v0.0.0-20231103220038-fd5c847140a6 + github.com/corazawaf/coraza-coreruleset/v4 v4.1.0 github.com/corazawaf/coraza/v3 v3.0.4 github.com/coreruleset/go-ftw v0.6.4 github.com/rs/zerolog v1.32.0 diff --git a/testing/coreruleset/go.sum b/testing/coreruleset/go.sum index 0b9d34825..3f86f6326 100644 --- a/testing/coreruleset/go.sum +++ b/testing/coreruleset/go.sum @@ -6,8 +6,8 @@ github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZC github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= -github.com/corazawaf/coraza-coreruleset v0.0.0-20231103220038-fd5c847140a6 h1:MjSFYff3j1L4zo3MNuqnQ19Jp5ps/sibntdtS/Kq/yk= -github.com/corazawaf/coraza-coreruleset v0.0.0-20231103220038-fd5c847140a6/go.mod h1:7rsocqNDkTCira5T0M7buoKR2ehh7YZiPkzxRuAgvVU= +github.com/corazawaf/coraza-coreruleset/v4 v4.1.0 h1:LJAitZ8DszT5eX+3VbGVg9PfsLEMpzPbARedIFURQSM= +github.com/corazawaf/coraza-coreruleset/v4 v4.1.0/go.mod h1:RQMGurig+irQq7v21yq7rM/9SAEf1bT6hCSplJ0ByKY= github.com/corazawaf/coraza/v3 v3.0.4 h1:Llemgoh0hp2NggCwcWN8lNiV4Pfe+AWzf1oEcasT234= github.com/corazawaf/coraza/v3 v3.0.4/go.mod h1:3fTYjY5BZv3nezLpH6NAap0gr3jZfbQWUAu2GF17ET4= github.com/corazawaf/libinjection-go v0.1.3 h1:PUplAYho1BBl0tIVbhDsNRuVGIeUYSiCEc9oQpb2rJU= @@ -18,7 +18,6 @@ github.com/coreruleset/ftw-tests-schema v1.1.0/go.mod h1:gRd9wBxjUI85HypWRDxJzbk github.com/coreruleset/go-ftw v0.6.4 h1:EdDNld38Jv4lxqHS+csGOJuHu1/8rpp4TlrFyoijTPk= github.com/coreruleset/go-ftw v0.6.4/go.mod h1:IayMjfOmmNNBcqTcZU92e6UZTy79/eFdmJEmRu8tLs4= 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= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= @@ -26,18 +25,14 @@ github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= -github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-yaml v1.11.3 h1:B3W9IdWbvrUu2OYQGwvU1nZtvMQJPBKgBUuweJjLj6I= github.com/goccy/go-yaml v1.11.3/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= @@ -63,7 +58,6 @@ github.com/knadh/koanf/v2 v2.1.0/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkX github.com/kyokomi/emoji/v2 v2.2.12 h1:sSVA5nH9ebR3Zji1o31wu3yOwD1zKXQA2z0zUyeit60= github.com/kyokomi/emoji/v2 v2.2.12/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 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/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -82,12 +76,10 @@ github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4 h1: github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= diff --git a/testing/engine/multiphase.go b/testing/engine/multiphase.go index 9ab13f5bc..9f6658e1a 100644 --- a/testing/engine/multiphase.go +++ b/testing/engine/multiphase.go @@ -186,3 +186,39 @@ SecRule REQUEST_URI|REQUEST_BODY "@rx test" "id:3, phase:2, deny, log, status:50 SecRule REQUEST_URI "@unconditionalMatch" "id:4, phase:1, pass, log" `, }) + +var _ = profile.RegisterProfile(profile.Profile{ + Meta: profile.Meta{ + Author: "M4tteoP", + Description: "Tests CRS ruleRemoveTargetById usage with multiphase and ARGS/ARGS_NAMES", + Enabled: true, + Name: "multiphase_ruleRemoveTargetById_args.yaml", + }, + Tests: []profile.Test{ + { + Title: "ruleRemoveTargetByIdWithARGS", + Stages: []profile.Stage{ + { + Stage: profile.SubStage{ + Input: profile.StageInput{ + URI: "/test/?fbclid=justanid", + Method: "GET", + }, + Output: profile.ExpectedOutput{ + TriggeredRules: []int{942441}, + NonTriggeredRules: []int{942440}, + }, + }, + }, + }, + }, + }, + // Rule 942441 should exclude ARGS:fbclid splitting it into excluding ARGS_GET:fbclidand ARGS_POST:fbclid, + // therefore rule 942440 should not be triggered. + Rules: ` +SecDebugLogLevel 9 + +SecRule ARGS_GET:fbclid "@unconditionalMatch" "id:942441, phase:2,pass,t:none,t:urlDecodeUni,ctl:ruleRemoveTargetById=942440;ARGS:fbclid" +SecRule ARGS_NAMES|ARGS "@rx justanid" "id:942440,phase:2,status:503,log,t:none,t:urlDecodeUni" +`, +})