Repository: corazawaf/coraza
Pull request #615 (2023-02-07 01:48:25)

Move debug formatting to collections and remove Map.Data
Currently there is a confusing `Data()` method on `Map` which looks very similar to `FindAll`. It's a bit more optimized for string formatting, but not by much.

This moves the string formatting to the collections themselves - it seems easier to reason about, more flexible, and more performant. Some other uses of Data are removed or made internal.

codecov[bot]: # [Codecov](https://codecov.io/gh/corazawaf/coraza/pull/615?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) Report
Base: **77.75**% // Head: **77.84**% // Increases project coverage by **`+0.09%`** :tada:
> Coverage data is based on head [(`2701bb9`)](https://codecov.io/gh/corazawaf/coraza/pull/615?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) compared to base [(`7f7f6fe`)](https://codecov.io/gh/corazawaf/coraza/commit/7f7f6fefb37c0f609be94fce15b4a055d9113b33?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf).
> Patch coverage: 92.30% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           v3/dev     #615      +/-   ##
==========================================
+ Coverage   77.75%   77.84%   +0.09%     
==========================================
  Files         144      144              
  Lines        7471     7479       +8     
==========================================
+ Hits         5809     5822      +13     
+ Misses       1431     1428       -3     
+ Partials      231      229       -2     
```

| Flag | Coverage Δ | |
|---|---|---|
| default | `73.47% <92.30%> (+0.09%)` | :arrow_up: |
| examples | `28.58% <1.92%> (-0.04%)` | :arrow_down: |
| ftw | `55.01% <7.69%> (-0.09%)` | :arrow_down: |
| ftw-multiphase | `55.11% <7.69%> (-0.09%)` | :arrow_down: |
| tinygo | `71.54% <92.30%> (+0.10%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/corazawaf/coraza/pull/615?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) | Coverage Δ | |
|---|---|---|
| [internal/corazawaf/transaction.go](https://codecov.io/gh/corazawaf/coraza/pull/615?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvY29yYXphd2FmL3RyYW5zYWN0aW9uLmdv) | `79.79% <60.00%> (-0.14%)` | :arrow_down: |
| [bodyprocessors/json.go](https://codecov.io/gh/corazawaf/coraza/pull/615?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-Ym9keXByb2Nlc3NvcnMvanNvbi5nbw==) | `87.09% <100.00%> (-0.41%)` | :arrow_down: |
| [internal/collections/map.go](https://codecov.io/gh/corazawaf/coraza/pull/615?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvY29sbGVjdGlvbnMvbWFwLmdv) | `93.63% <100.00%> (+3.34%)` | :arrow_up: |
| [internal/collections/named.go](https://codecov.io/gh/corazawaf/coraza/pull/615?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvY29sbGVjdGlvbnMvbmFtZWQuZ28=) | `91.89% <100.00%> (+2.60%)` | :arrow_up: |
| [internal/collections/single.go](https://codecov.io/gh/corazawaf/coraza/pull/615?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvY29sbGVjdGlvbnMvc2luZ2xlLmdv) | `100.00% <100.00%> (ø)` | |
| [internal/collections/sized.go](https://codecov.io/gh/corazawaf/coraza/pull/615?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvY29sbGVjdGlvbnMvc2l6ZWQuZ28=) | `81.81% <100.00%> (+1.17%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/corazawaf/coraza/pull/615?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf).


Patch:
diff --git a/.github/workflows/tinygo.yml b/.github/workflows/tinygo.yml
index 35bc5df..14f95d9 100644
--- a/.github/workflows/tinygo.yml
+++ b/.github/workflows/tinygo.yml
@@ -20,12 +20,9 @@ jobs:
   test:
     strategy:
       matrix:
-        # TODO(anuraaga): Add 1.19.x after TinyGo adds support for it.
-        go-version: [1.18.x]
+        go-version: [1.19.x]
         os: [ubuntu-latest]
     runs-on: ${{ matrix.os }}
-    container:
-      image: tinygo/tinygo:0.24.0
     steps:
       - name: Checkout code
         uses: actions/checkout@v3
@@ -36,6 +33,11 @@ jobs:
           go-version: ${{ matrix.go-version }}
           cache: true
 
+      - name: setup tinygo
+        uses: acifani/setup-tinygo@v1
+        with:
+          tinygo-version: 0.26.0
+
       - name: Cache TinyGo build
         uses: actions/cache@v3
         with:
diff --git a/bodyprocessors/json.go b/bodyprocessors/json.go
index ef9f2cd..d8cce85 100644
--- a/bodyprocessors/json.go
+++ b/bodyprocessors/json.go
@@ -25,11 +25,7 @@ func (js *jsonBodyProcessor) ProcessRequest(reader io.Reader, v rules.Transactio
 	argsGetCol := v.ArgsGet()
 	for key, value := range data {
 		// TODO: This hack prevent GET variables from overriding POST variables
-		for k := range argsGetCol.Data() {
-			if k == key {
-				argsGetCol.Remove(k)
-			}
-		}
+		argsGetCol.Remove(key)
 		col.SetIndex(key, 0, value)
 	}
 	return nil
diff --git a/collection/collection.go b/collection/collection.go
index 15464ad..37d077f 100644
--- a/collection/collection.go
+++ b/collection/collection.go
@@ -69,7 +69,4 @@ type Map interface {
 
 	// Remove deletes the key from the CollectionMap
 	Remove(key string)
-
-	// Data returns all the data in the CollectionMap
-	Data() map[string][]string
 }
diff --git a/config.go b/config.go
index 9495da3..1e4b83d 100644
--- a/config.go
+++ b/config.go
@@ -70,14 +70,12 @@ type WAFConfig interface {
 	WithRootFS(fs fs.FS) WAFConfig
 }
 
-const unsetLimit = -1
-
 // NewWAFConfig creates a new WAFConfig with the default settings.
 func NewWAFConfig() WAFConfig {
 	return &wafConfig{
-		requestBodyLimit:         unsetLimit,
-		requestBodyInMemoryLimit: unsetLimit,
-		responseBodyLimit:        unsetLimit,
+		requestBodyLimit:         corazawaf.UnsetLimit,
+		requestBodyInMemoryLimit: corazawaf.UnsetLimit,
+		responseBodyLimit:        corazawaf.UnsetLimit,
 	}
 }
 
diff --git a/coraza.conf-recommended b/coraza.conf-recommended
index b05b4b0..b5166d4 100644
--- a/coraza.conf-recommended
+++ b/coraza.conf-recommended
@@ -42,6 +42,9 @@ SecRule REQUEST_HEADERS:Content-Type "^application/json" \
 # low as practical.
 #
 SecRequestBodyLimit 13107200
+
+SecRequestBodyInMemoryLimit 131072
+
 SecRequestBodyNoFilesLimit 131072
 
 # What to do if the request body size is above our configured limit.
diff --git a/http/middleware.go b/http/middleware.go
index 7b53051..85d897b 100644
--- a/http/middleware.go
+++ b/http/middleware.go
@@ -107,6 +107,11 @@ func processRequest(tx types.Transaction, req *http.Request) (*types.Interruptio
 }
 
 func WrapHandler(waf coraza.WAF, l Logger, h http.Handler) http.Handler {
+	if waf == nil {
+		l("nil WAF passed to the handler wrapper")
+		return h
+	}
+
 	fn := func(w http.ResponseWriter, r *http.Request) {
 		tx := waf.NewTransaction()
 		defer func() {
diff --git a/internal/collections/map.go b/internal/collections/map.go
index d20b84c..a55c34a 100644
--- a/internal/collections/map.go
+++ b/internal/collections/map.go
@@ -22,7 +22,7 @@ type Map struct {
 
 var _ collection.Map = &Map{}
 
-func NewMap(variable variables.RuleVariable) collection.Map {
+func NewMap(variable variables.RuleVariable) *Map {
 	return &Map{
 		name:     variable.Name(),
 		variable: variable,
@@ -144,15 +144,23 @@ func (c *Map) Reset() {
 	}
 }
 
-func (c *Map) Data() map[string][]string {
-	result := map[string][]string{}
+func (c *Map) String() string {
+	res := strings.Builder{}
+	res.WriteString(c.name)
+	res.WriteString(":\n")
 	for k, v := range c.data {
-		result[k] = make([]string, 0, len(v))
-		for _, a := range v {
-			result[k] = append(result[k], a.value)
+		res.WriteString("    ")
+		res.WriteString(k)
+		res.WriteString(": ")
+		for i, vv := range v {
+			if i > 0 {
+				res.WriteString(",")
+			}
+			res.WriteString(vv.value)
 		}
+		res.WriteByte('\n')
 	}
-	return result
+	return res.String()
 }
 
 // keyValue stores the case preserved original key and value
diff --git a/internal/collections/named.go b/internal/collections/named.go
index fb19d86..bad5501 100644
--- a/internal/collections/named.go
+++ b/internal/collections/named.go
@@ -4,7 +4,9 @@
 package collections
 
 import (
+	"fmt"
 	"regexp"
+	"strings"
 
 	"github.com/corazawaf/coraza/v3/collection"
 	"github.com/corazawaf/coraza/v3/internal/corazarules"
@@ -14,7 +16,7 @@ import (
 
 // NamedCollection is a Collection that also keeps track of unique names.
 type NamedCollection struct {
-	collection.Map
+	*Map
 	names []string
 }
 
@@ -57,6 +59,18 @@ func (c *NamedCollection) Remove(key string) {
 	}
 }
 
+// Data is an internal method used for serializing to JSON
+func (c *NamedCollection) Data() map[string][]string {
+	result := map[string][]string{}
+	for k, v := range c.data {
+		result[k] = make([]string, 0, len(v))
+		for _, a := range v {
+			result[k] = append(result[k], a.value)
+		}
+	}
+	return result
+}
+
 // Name returns the name for the current CollectionMap
 func (c *NamedCollection) Name() string {
 	return c.Map.Name()
@@ -67,10 +81,6 @@ func (c *NamedCollection) Reset() {
 	c.names = nil
 }
 
-func (c *NamedCollection) Data() map[string][]string {
-	return c.Map.Data()
-}
-
 func (c *NamedCollection) Names(rv variables.RuleVariable) collection.Collection {
 	return &NamedCollectionNames{
 		name:       rv.Name(),
@@ -79,6 +89,10 @@ func (c *NamedCollection) Names(rv variables.RuleVariable) collection.Collection
 	}
 }
 
+func (c *NamedCollection) String() string {
+	return fmt.Sprint(c.Map)
+}
+
 func (c *NamedCollection) addName(key string) {
 	for _, n := range c.names {
 		if n == key {
@@ -120,3 +134,16 @@ func (c *NamedCollectionNames) Name() string {
 
 func (c *NamedCollectionNames) Reset() {
 }
+
+func (c *NamedCollectionNames) String() string {
+	res := strings.Builder{}
+	res.WriteString(c.name)
+	res.WriteString(": ")
+	for i, k := range c.collection.names {
+		if i > 0 {
+			res.WriteString(",")
+		}
+		res.WriteString(k)
+	}
+	return res.String()
+}
diff --git a/internal/collections/single.go b/internal/collections/single.go
index 9b42edb..4848afb 100644
--- a/internal/collections/single.go
+++ b/internal/collections/single.go
@@ -4,6 +4,8 @@
 package collections
 
 import (
+	"fmt"
+
 	"github.com/corazawaf/coraza/v3/collection"
 	"github.com/corazawaf/coraza/v3/internal/corazarules"
 	"github.com/corazawaf/coraza/v3/types"
@@ -51,3 +53,7 @@ func (c *Single) Name() string {
 func (c *Single) Reset() {
 	c.data = ""
 }
+
+func (c *Single) String() string {
+	return fmt.Sprintf("%s: %s", c.name, c.data)
+}
diff --git a/internal/collections/sized.go b/internal/collections/sized.go
index 82e8206..5de5386 100644
--- a/internal/collections/sized.go
+++ b/internal/collections/sized.go
@@ -4,6 +4,7 @@
 package collections
 
 import (
+	"fmt"
 	"regexp"
 	"strconv"
 
@@ -62,14 +63,18 @@ func (c *SizeCollection) Reset() {
 	// do nothing
 }
 
+func (c *SizeCollection) String() string {
+	return fmt.Sprintf("%s: %d", c.name, c.size())
+}
+
 // Size returns the size of all the collections values
 func (c *SizeCollection) size() int {
 	i := 0
 	for _, d := range c.data {
 		// we iterate over d
-		for _, data := range d.Data() {
+		for _, data := range d.data {
 			for _, v := range data {
-				i += len(v)
+				i += len(v.value)
 			}
 		}
 	}
diff --git a/internal/corazawaf/transaction.go b/internal/corazawaf/transaction.go
index bc03f0b..e0180b3 100644
--- a/internal/corazawaf/transaction.go
+++ b/internal/corazawaf/transaction.go
@@ -1396,54 +1396,27 @@ func (tx *Transaction) Close() error {
 	}
 }
 
+// String will return a string with the transaction debug information
 func (tx *Transaction) String() string {
-	return tx.Debug()
-}
-
-// Debug will return a string with the transaction debug information
-func (tx *Transaction) Debug() string {
-	res := "\n\n----------------------- ERRORLOG ----------------------\n"
+	res := strings.Builder{}
+	res.WriteString("\n\n----------------------- ERRORLOG ----------------------\n")
 	for _, mr := range tx.matchedRules {
 		status, _ := strconv.Atoi(tx.variables.responseStatus.Get())
-		res += mr.ErrorLog(status)
-		res += "\n\n----------------------- MATCHDATA ---------------------\n"
+		res.WriteString(mr.ErrorLog(status))
+		res.WriteString("\n\n----------------------- MATCHDATA ---------------------\n")
 		for _, md := range mr.MatchedDatas() {
-			res += fmt.Sprintf("%+v", md) + "\n"
+			fmt.Fprintf(&res, "%+v\n", md)
 		}
-		res += "\n"
+		res.WriteByte('\n')
 	}
 
-	res += "\n------------------------ DEBUG ------------------------\n"
+	res.WriteString("\n------------------------ DEBUG ------------------------\n")
 	for v := byte(1); v < types.VariablesCount; v++ {
 		vr := variables.RuleVariable(v)
-		if vr.Name() == "UNKNOWN" {
-			continue
-		}
-		data := map[string][]string{}
-		switch col := tx.Collection(vr).(type) {
-		case *collections.Single:
-			data[""] = []string{
-				col.Get(),
-			}
-		case collection.Map:
-			data = col.Data()
-		}
-
-		if len(data) == 1 {
-			res += fmt.Sprintf("%s: ", vr.Name())
-		} else {
-			res += fmt.Sprintf("%s:\n", vr.Name())
-		}
-
-		for k, d := range data {
-			if k != "" {
-				res += fmt.Sprintf("    %s: %s\n", k, strings.Join(d, ","))
-			} else {
-				res += fmt.Sprintf("%s\n", strings.Join(d, ","))
-			}
-		}
+		col := tx.Collection(vr)
+		fmt.Fprint(&res, col)
 	}
-	return res
+	return res.String()
 }
 
 // generateReqbodyError generates all the error variables for the request body parser
diff --git a/internal/corazawaf/waf.go b/internal/corazawaf/waf.go
index 653bccb..23cbf4d 100644
--- a/internal/corazawaf/waf.go
+++ b/internal/corazawaf/waf.go
@@ -4,6 +4,7 @@
 package corazawaf
 
 import (
+	"errors"
 	"fmt"
 	"io"
 	"io/fs"
@@ -256,35 +257,25 @@ func NewWAF() *WAF {
 		logger: &log.Logger{},
 		Level:  loggers.LogLevelInfo,
 	}
+
 	logWriter, err := loggers.GetLogWriter("serial")
 	if err != nil {
 		logger.Error("error creating serial log writer: %s", err.Error())
 	}
+
 	waf := &WAF{
 		// Initializing pool for transactions
-		txPool:                   sync.NewPool(func() interface{} { return new(Transaction) }),
-		ArgumentSeparator:        "&",
-		AuditLogWriter:           logWriter,
-		AuditEngine:              types.AuditEngineOff,
-		AuditLogParts:            types.AuditLogParts("ABCFHZ"),
+		txPool: sync.NewPool(func() interface{} { return new(Transaction) }),
+		// These defaults are unavoidable as they are zero values for the variables
+		RuleEngine:               types.RuleEngineOn,
 		RequestBodyAccess:        false,
-		RequestBodyInMemoryLimit: 131072,
-		RequestBodyLimit:         134217728, // 10mb
-		RequestBodyLimitAction:   types.BodyLimitActionReject,
-		ResponseBodyMimeTypes:    []string{"text/html", "text/plain"},
-		ResponseBodyLimit:        524288,
-		ResponseBodyLimitAction:  types.BodyLimitActionReject,
+		RequestBodyLimit:         _1gb,
+		RequestBodyInMemoryLimit: _1gb,
 		ResponseBodyAccess:       false,
-		RuleEngine:               types.RuleEngineOn,
-		Rules:                    NewRuleGroup(),
-		TmpDir:                   "/tmp",
-		AuditLogRelevantStatus:   regexp.MustCompile(`.*`),
+		ResponseBodyLimit:        _1gb,
+		AuditLogWriter:           logWriter,
 		Logger:                   logger,
 	}
-	// We initialize a basic audit log writer that discards output
-	if err := logWriter.Init(types.Config{}); err != nil {
-		fmt.Println(err)
-	}
 	if err := waf.SetDebugLogPath(""); err != nil {
 		fmt.Println(err)
 	}
@@ -305,3 +296,38 @@ func (w *WAF) SetDebugLogLevel(lvl int) error {
 func (w *WAF) SetErrorCallback(cb func(rule types.MatchedRule)) {
 	w.ErrorLogCb = cb
 }
+
+const (
+	_1gb       = 1073741824
+	UnsetLimit = -1
+)
+
+func (w *WAF) Validate() error {
+	if w.RequestBodyLimit <= 0 {
+		return errors.New("request body limit should be bigger than 0")
+	}
+
+	if w.RequestBodyLimit > _1gb {
+		return errors.New("request body limit should be at most 1GB")
+	}
+
+	if w.RequestBodyLimit != UnsetLimit {
+		if w.RequestBodyLimit < w.RequestBodyInMemoryLimit {
+			return fmt.Errorf("request body limit should be at least the memory limit")
+		}
+	}
+
+	if w.RequestBodyInMemoryLimit <= 0 {
+		return errors.New("request body memory limit should be bigger than 0")
+	}
+
+	if w.ResponseBodyLimit <= 0 {
+		return errors.New("response body limit should be bigger than 0")
+	}
+
+	if w.ResponseBodyLimit > _1gb {
+		return errors.New("response body limit should be at most 1GB")
+	}
+
+	return nil
+}
diff --git a/internal/seclang/directives.go b/internal/seclang/directives.go
index 247c345..4f81798 100644
--- a/internal/seclang/directives.go
+++ b/internal/seclang/directives.go
@@ -478,6 +478,7 @@ func directiveSecAuditLog(options *DirectiveOptions) error {
 	if len(options.Opts) == 0 {
 		return errors.New("syntax error: SecAuditLog /some/absolute/path.log")
 	}
+
 	options.Config.Set("auditlog_file", options.Opts)
 	if err := options.WAF.AuditLogWriter.Init(options.Config); err != nil {
 		return err
@@ -513,6 +514,7 @@ func directiveSecAuditLogFormat(options *DirectiveOptions) error {
 		return err
 	}
 	options.Config.Set("auditlog_formatter", formatter)
+
 	if err := options.WAF.AuditLogWriter.Init(options.Config); err != nil {
 		return err
 	}
diff --git a/loggers/serial_writer.go b/loggers/serial_writer.go
index a968d83..4cbeac4 100644
--- a/loggers/serial_writer.go
+++ b/loggers/serial_writer.go
@@ -45,6 +45,10 @@ func (sl *serialWriter) Init(c types.Config) error {
 }
 
 func (sl *serialWriter) Write(al *AuditLog) error {
+	if sl.formatter == nil {
+		return nil
+	}
+
 	bts, err := sl.formatter(al)
 	if err != nil {
 		return err
diff --git a/waf.go b/waf.go
index 69f74f4..be3016b 100644
--- a/waf.go
+++ b/waf.go
@@ -4,7 +4,6 @@
 package coraza
 
 import (
-	"errors"
 	"fmt"
 
 	"github.com/corazawaf/coraza/v3/internal/corazawaf"
@@ -25,8 +24,6 @@ type WAF interface {
 	NewTransactionWithID(id string) types.Transaction
 }
 
-const _1gb = 1073741824
-
 // NewWAF creates a new WAF instance with the provided configuration.
 func NewWAF(config WAFConfig) (WAF, error) {
 	c := config.(*wafConfig)
@@ -79,28 +76,11 @@ func NewWAF(config WAFConfig) (WAF, error) {
 		waf.RequestBodyAccess = true
 	}
 
-	if c.requestBodyLimit != unsetLimit {
-		if c.requestBodyLimit <= 0 {
-			return nil, errors.New("request body limit should be bigger than 0")
-		}
-
-		if c.requestBodyLimit > _1gb {
-			return nil, errors.New("request body limit should be at most 1GB")
-		}
-
+	if c.requestBodyLimit != corazawaf.UnsetLimit {
 		waf.RequestBodyLimit = int64(c.requestBodyLimit)
 	}
 
-	if c.requestBodyInMemoryLimit != unsetLimit {
-		if c.requestBodyLimit != unsetLimit {
-			if c.requestBodyLimit < c.requestBodyInMemoryLimit {
-				return nil, errors.New("request body limit should be at least the memory limit")
-			}
-		}
-
-		if c.requestBodyInMemoryLimit <= 0 {
-			return nil, errors.New("request body memory limit should be bigger than 0")
-		}
+	if c.requestBodyInMemoryLimit != corazawaf.UnsetLimit {
 		waf.RequestBodyInMemoryLimit = int64(c.requestBodyInMemoryLimit)
 	}
 
@@ -108,15 +88,7 @@ func NewWAF(config WAFConfig) (WAF, error) {
 		waf.ResponseBodyAccess = true
 	}
 
-	if c.responseBodyLimit != unsetLimit {
-		if c.responseBodyLimit <= 0 {
-			return nil, errors.New("response body limit should be bigger than 0")
-		}
-
-		if c.responseBodyLimit > _1gb {
-			return nil, errors.New("response body limit should be at most 1GB")
-		}
-
+	if c.responseBodyLimit != corazawaf.UnsetLimit {
 		waf.ResponseBodyLimit = int64(c.responseBodyLimit)
 	}
 
@@ -124,6 +96,10 @@ func NewWAF(config WAFConfig) (WAF, error) {
 		waf.ErrorLogCb = c.errorCallback
 	}
 
+	if err := waf.Validate(); err != nil {
+		return nil, err
+	}
+
 	return wafWrapper{waf: waf}, nil
 }
 


Test patch:
diff --git a/http/middleware_test.go b/http/middleware_test.go
index dd2abcc..3a3e350 100644
--- a/http/middleware_test.go
+++ b/http/middleware_test.go
@@ -483,6 +483,14 @@ func TestObtainStatusCodeFromInterruptionOrDefault(t *testing.T) {
 	}
 }
 
+func TestHandlerWithNilWAF(t *testing.T) {
+	delegateHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
+	wrappedHandler := WrapHandler(nil, t.Logf, delegateHandler).(http.HandlerFunc)
+	if want, have := fmt.Sprintf("%v", delegateHandler), fmt.Sprintf("%v", wrappedHandler); want != have {
+		t.Errorf("unexpected wrapped handler")
+	}
+}
+
 func TestHandlerAPI(t *testing.T) {
 	testCases := map[string]struct {
 		handler            http.HandlerFunc
@@ -538,7 +546,11 @@ func TestHandlerAPI(t *testing.T) {
 		},
 	}
 
-	waf, _ := coraza.NewWAF(coraza.NewWAFConfig().WithRequestBodyLimit(3))
+	waf, err := coraza.NewWAF(coraza.NewWAFConfig().WithRequestBodyLimit(3).WithRequestBodyInMemoryLimit(3))
+	if err != nil {
+		t.Fatalf("unexpected error while creating the WAF: %s", err.Error())
+	}
+
 	for name, tCase := range testCases {
 		t.Run(name, func(t *testing.T) {
 			srv := httptest.NewServer(WrapHandler(waf, t.Logf, tCase.handler))
diff --git a/internal/collections/map_test.go b/internal/collections/map_test.go
index 4f2a8a7..346e798 100644
--- a/internal/collections/map_test.go
+++ b/internal/collections/map_test.go
@@ -14,6 +14,7 @@
 package collections
 
 import (
+	"fmt"
 	"regexp"
 	"testing"
 
@@ -36,4 +37,22 @@ func TestMap(t *testing.T) {
 	if l := len(c.FindRegex(regexp.MustCompile("k.*"))); l != 2 {
 		t.Errorf("Error should find regex, got %d", l)
 	}
+
+	c.Add("key2", "value3")
+
+	wantStr := `ARGS_POST:
+    key: value
+    key2: value2,value3
+`
+
+	if have := fmt.Sprint(c); have != wantStr {
+		// Map order is not guaranteed, not pretty but checking twice is the simplest for now.
+		wantStr = `ARGS_POST:
+    key2: value2,value3
+    key: value
+`
+		if have != wantStr {
+			t.Errorf("String() = %q, want %q", have, wantStr)
+		}
+	}
 }
diff --git a/internal/collections/named_test.go b/internal/collections/named_test.go
index 5235a6a..ee19127 100644
--- a/internal/collections/named_test.go
+++ b/internal/collections/named_test.go
@@ -4,6 +4,7 @@
 package collections
 
 import (
+	"fmt"
 	"regexp"
 	"testing"
 
@@ -28,6 +29,20 @@ func TestNamedCollection(t *testing.T) {
 	if l := len(c.FindRegex(regexp.MustCompile("k.*"))); l != 2 {
 		t.Errorf("Error should find regex, got %d", l)
 	}
+	wantStr := `ARGS_POST:
+    key: value
+    key2: value2
+`
+	if have := fmt.Sprint(c); have != wantStr {
+		// Map order is not guaranteed, not pretty but checking twice is the simplest for now.
+		wantStr = `ARGS_POST:
+    key2: value2
+    key: value
+`
+		if have != wantStr {
+			t.Errorf("String() = %q, want %q", have, wantStr)
+		}
+	}
 
 	// Now test names
 
@@ -37,12 +52,24 @@ func TestNamedCollection(t *testing.T) {
 	}
 
 	assertValuesMatch(t, names.FindAll(), "key", "key2")
+	if want, have := "ARGS_POST_NAMES: key,key2", fmt.Sprint(names); want != have {
+		t.Errorf("want %q, have %q", want, have)
+	}
 	c.Add("key", "value2")
 	assertValuesMatch(t, names.FindAll(), "key", "key2")
+	if want, have := "ARGS_POST_NAMES: key,key2", fmt.Sprint(names); want != have {
+		t.Errorf("want %q, have %q", want, have)
+	}
 	// While selection operators will treat this as case-insensitive, names should have all names
 	// as-is.
 	c.Add("Key", "value3")
 	assertValuesMatch(t, names.FindAll(), "key", "key2", "Key")
+	if want, have := "ARGS_POST_NAMES: key,key2,Key", fmt.Sprint(names); want != have {
+		t.Errorf("want %q, have %q", want, have)
+	}
 	c.Remove("key2")
 	assertValuesMatch(t, names.FindAll(), "key", "Key")
+	if want, have := "ARGS_POST_NAMES: key,Key", fmt.Sprint(names); want != have {
+		t.Errorf("want %q, have %q", want, have)
+	}
 }
diff --git a/internal/collections/single_test.go b/internal/collections/single_test.go
index ac37420..8e2ef01 100644
--- a/internal/collections/single_test.go
+++ b/internal/collections/single_test.go
@@ -20,6 +20,10 @@ func TestSingle(t *testing.T) {
 		t.Errorf("want %s, have %s", want, have)
 	}
 
+	if want, have := "ARGS_PATH: ", c.String(); want != have {
+		t.Errorf("want %s, have %s", want, have)
+	}
+
 	assertValuesMatch(t, c.FindAll())
 
 	c.Set("bear")
@@ -28,5 +32,9 @@ func TestSingle(t *testing.T) {
 		t.Errorf("want %s, have %s", want, have)
 	}
 
+	if want, have := "ARGS_PATH: bear", c.String(); want != have {
+		t.Errorf("want %s, have %s", want, have)
+	}
+
 	assertValuesMatch(t, c.FindAll(), "bear")
 }
diff --git a/internal/collections/sized_test.go b/internal/collections/sized_test.go
index 168d39f..c90705d 100644
--- a/internal/collections/sized_test.go
+++ b/internal/collections/sized_test.go
@@ -15,10 +15,22 @@ func TestSizedCollection(t *testing.T) {
 	proxy := NewSizeCollection(variables.ArgsCombinedSize, c1, c2)
 
 	assertValuesMatch(t, proxy.FindAll(), "0")
+	if want, have := "ARGS_COMBINED_SIZE: 0", proxy.String(); want != have {
+		t.Errorf("want %s, have %s", want, have)
+	}
 	c1.Set("key1", []string{"value1", "value2"})
 	assertValuesMatch(t, proxy.FindAll(), "12")
+	if want, have := "ARGS_COMBINED_SIZE: 12", proxy.String(); want != have {
+		t.Errorf("want %s, have %s", want, have)
+	}
 	c1.Set("key2", []string{"value2"})
 	assertValuesMatch(t, proxy.FindAll(), "18")
+	if want, have := "ARGS_COMBINED_SIZE: 18", proxy.String(); want != have {
+		t.Errorf("want %s, have %s", want, have)
+	}
 	c2.Set("key3", []string{"value3"})
 	assertValuesMatch(t, proxy.FindAll(), "24")
+	if want, have := "ARGS_COMBINED_SIZE: 24", proxy.String(); want != have {
+		t.Errorf("want %s, have %s", want, have)
+	}
 }
diff --git a/internal/corazawaf/transaction_test.go b/internal/corazawaf/transaction_test.go
index 1e1e260..6b7512b 100644
--- a/internal/corazawaf/transaction_test.go
+++ b/internal/corazawaf/transaction_test.go
@@ -488,10 +488,15 @@ func TestWriteResponseBody(t *testing.T) {
 						t.Run(name, func(t *testing.T) {
 							waf := NewWAF()
 							waf.RuleEngine = types.RuleEngineOn
+							waf.ResponseBodyMimeTypes = []string{"text/plain"}
 							waf.ResponseBodyAccess = true
 							waf.ResponseBodyLimit = int64(testCase.responseBodyLimit)
 							waf.ResponseBodyLimitAction = testCase.responseBodyLimitAction
 
+							if err := waf.Validate(); err != nil {
+								t.Fatalf("failed to validate the WAF: %s", err.Error())
+							}
+
 							tx := waf.NewTransaction()
 							tx.AddResponseHeader("content-type", "text/plain")
 
@@ -871,6 +876,7 @@ func TestTxPhase4Magic(t *testing.T) {
 	waf.ResponseBodyAccess = true
 	waf.ResponseBodyLimit = 3
 	waf.ResponseBodyLimitAction = types.BodyLimitActionProcessPartial
+	waf.ResponseBodyMimeTypes = []string{"text/html"}
 	tx := waf.NewTransaction()
 	tx.AddResponseHeader("content-type", "text/html")
 	if it, _, err := tx.WriteResponseBody([]byte("more bytes")); it != nil || err != nil {
@@ -1106,7 +1112,7 @@ func validateMacroExpansion(tests map[string]string, tx *Transaction, t *testing
 		res := m.Expand(tx)
 		if res != v {
 			if testing.Verbose() {
-				fmt.Println(tx.Debug())
+				fmt.Println(tx)
 				fmt.Println("===STACK===\n", string(debug.Stack())+"\n===STACK===")
 			}
 			t.Error("Failed set transaction for " + k + ", expected " + v + ", got " + res)
diff --git a/internal/seclang/rules_test.go b/internal/seclang/rules_test.go
index 3aa70bc..4919f39 100644
--- a/internal/seclang/rules_test.go
+++ b/internal/seclang/rules_test.go
@@ -314,8 +314,9 @@ func TestTxIssue147(t *testing.T) {
 	tx := waf.NewTransaction()
 	// response body access is required
 	tx.ResponseBodyAccess = true
-	// we need a content-type header
+	tx.WAF.ResponseBodyMimeTypes = []string{"text/html"}
 	tx.AddResponseHeader("Content-Type", "text/html")
+
 	if tx.IsResponseBodyProcessable() {
 		if it, _, err := tx.WriteResponseBody([]byte("#!/usr/bin/python")); it != nil || err != nil {
 			t.Error(err)
diff --git a/testing/engine_test.go b/testing/engine_test.go
index 68eef74..215d11f 100644
--- a/testing/engine_test.go
+++ b/testing/engine_test.go
@@ -34,7 +34,7 @@ func TestDebug(t *testing.T) {
 	if err := test.RunPhases(); err != nil {
 		t.Error(err)
 	}
-	debug := fmt.Sprintf("%s", test.transaction)
+	debug := fmt.Sprint(test.transaction)
 	expected := []string{
 		"REQUEST_URI: /test",
 		"REQUEST_METHOD: OPTIONS",