Repository: corazawaf/coraza
Pull request #517 (2022-11-25 17:01:32)

missing hostname in logs
Hi there,
taking a look at https://github.com/corazawaf/coraza/issues/515, indeed `resolvedIP` variable is never populated.
To my eyes seems that the right value could be the `ServerIPAddress`, populated at the beginning of the transaction, when `ProcessConnection` is called:
https://github.com/corazawaf/coraza/blob/bffb4359d11967e9d13c7ec67a35868b1553ad1e/internal/corazawaf/transaction.go#L638


Still, taking a look at the [connector](https://github.com/corazawaf/coraza-spoa/blob/main/internal/request.go#L142), this fix would just populate the field with the IP and may not be useful for @mac-chaffee usecase.
If it sounds reasonable I can, as proposed, catch the `Host` header adding it to a new `tx.variables`, but I'm wondering about sanitizing the retrieved value 🤔.


Attempt to close https://github.com/corazawaf/coraza/issues/515.

mac-chaffee: It does look like modsecurity sets that badly-named "hostname" variable to an IP address: https://github.com/SpiderLabs/ModSecurity/blob/4127c1bf52d2b30a5c2c3e641b8085fd9a720f43/src/rule_message.cc#L45

Now that I think of it, I normally find out the real hostname by looking at the modsecurity audit logs which includes the full HTTP request that triggered the rule. Doesn't seem like that kind of audit logging exists in Coraza yet, so I do like the idea of an extra section in the existing logs in the meantime.

I'm not too worried about sanitization since Coraza is designed to log attack attempts anyway.
jcchavezs: @jptosso @fzipi could you chime in?
fzipi: @mac-chaffee Depends on the tool you are using to look at logs. I would say we should sanitize it before sending it out to a filesystem/tool. More information [here](https://cwe.mitre.org/data/definitions/117.html).
fzipi: I think that `resolveIP` was meant to get the actual name from the [ServerIPAddress()](https://github.com/corazawaf/coraza/blob/9d003ed52753367b75383566ca69bc3d467cc8a0/internal/corazarules/rule_match.go#L104). I don't know if we want to do an extra name resolution before logging, but if someone needs it we might use a config parameter like `HostnameLookups Off` from Apache.

So the quick solution is to use the `ServerIPAddress` for the resolveIP (probably adding a comment also), and in the future we can add configurable reverse resolution if needed.
codecov-commenter: # [Codecov](https://codecov.io/gh/corazawaf/coraza/pull/517?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) Report
Base: **78.44**% // Head: **78.43**% // Decreases project coverage by **`-0.00%`** :warning:
> Coverage data is based on head [(`f55f705`)](https://codecov.io/gh/corazawaf/coraza/pull/517?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) compared to base [(`6470648`)](https://codecov.io/gh/corazawaf/coraza/commit/64706480a6a669815a33d2afaa234b6f9ba21830?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf).
> Patch coverage: 100.00% of modified lines in pull request are covered.

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


```diff
@@            Coverage Diff             @@
##           v3/dev     #517      +/-   ##
==========================================
- Coverage   78.44%   78.43%   -0.01%     
==========================================
  Files         145      145              
  Lines        6981     6980       -1     
==========================================
- Hits         5476     5475       -1     
  Misses       1274     1274              
  Partials      231      231              
```

| Flag | Coverage Δ | |
|---|---|---|
| default | `73.86% <100.00%> (-0.01%)` | :arrow_down: |
| examples | `27.37% <100.00%> (-0.02%)` | :arrow_down: |
| ftw | `56.59% <100.00%> (-0.01%)` | :arrow_down: |
| tinygo | `71.98% <100.00%> (-0.01%)` | :arrow_down: |

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/517?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) | Coverage Δ | |
|---|---|---|
| [internal/corazarules/rule\_match.go](https://codecov.io/gh/corazawaf/coraza/pull/517/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvY29yYXphcnVsZXMvcnVsZV9tYXRjaC5nbw==) | `49.53% <100.00%> (-0.47%)` | :arrow_down: |

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/517?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/fuzz.yml b/.github/workflows/fuzz.yml
new file mode 100644
index 0000000..5c62d72
--- /dev/null
+++ b/.github/workflows/fuzz.yml
@@ -0,0 +1,24 @@
+name: Fuzz tests
+
+on:
+  schedule:
+    # https://crontab.guru/#05_14_*_*_*
+    - cron: '05 14 * * *'
+  workflow_dispatch:
+
+jobs:
+  fuzz:
+    name: Fuzz tests
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-go@v1
+        with:
+          go-version: '>=1.18.0'
+      - run: go run mage.go fuzz
+      - run: |
+          gh issue create --title "$GITHUB_WORKFLOW #$GITHUB_RUN_NUMBER failed" \
+                          --body "See [$GITHUB_WORKFLOW #$GITHUB_RUN_NUMBER](https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)."
+        if: failure() && github.run_attempt == 1
+        env:
+          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cf1ed95..8f7e29b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,7 +24,7 @@ IMPROVEMENTS:
 
 NOTES:
 
-**Migrate from Corza v1**
+**Migrate from Coraza v1**
 
 * Rollback `SecAuditLog` to the legacy syntax (serial/concurrent)
 * Attach an error log handler using `waf.SetErrorLogCb(cb)` (optional)
diff --git a/bodyprocessors/multipart.go b/bodyprocessors/multipart.go
index 52402a5..7effc84 100644
--- a/bodyprocessors/multipart.go
+++ b/bodyprocessors/multipart.go
@@ -4,6 +4,7 @@
 package bodyprocessors
 
 import (
+	"errors"
 	"fmt"
 	"io"
 	"log"
@@ -16,18 +17,17 @@ import (
 	"github.com/corazawaf/coraza/v3/rules"
 )
 
-type multipartBodyProcessor struct {
-}
+type multipartBodyProcessor struct{}
 
 func (mbp *multipartBodyProcessor) ProcessRequest(reader io.Reader, v rules.TransactionVariables, options Options) error {
 	mimeType := options.Mime
 	storagePath := options.StoragePath
 	mediaType, params, err := mime.ParseMediaType(mimeType)
 	if err != nil {
-		log.Fatal(err)
+		log.Fatalf("failed to parse media type: %s", err.Error())
 	}
 	if !strings.HasPrefix(mediaType, "multipart/") {
-		return fmt.Errorf("not a multipart body")
+		return errors.New("not a multipart body")
 	}
 	mr := multipart.NewReader(reader, params["boundary"])
 	totalSize := int64(0)
@@ -93,7 +93,7 @@ func (mbp *multipartBodyProcessor) ProcessRequest(reader io.Reader, v rules.Tran
 	return nil
 }
 
-func (mbp *multipartBodyProcessor) ProcessResponse(reader io.Reader, v rules.TransactionVariables, options Options) error {
+func (mbp *multipartBodyProcessor) ProcessResponse(_ io.Reader, _ rules.TransactionVariables, options Options) error {
 	return nil
 }
 
diff --git a/config.go b/config.go
index 2503818..ceda8fb 100644
--- a/config.go
+++ b/config.go
@@ -39,8 +39,10 @@ type WAFConfig interface {
 	// WithDebugLogger configures a debug logger.
 	WithDebugLogger(logger loggers.DebugLogger) WAFConfig
 
-	// WithErrorLogger configures an error logger.
-	WithErrorLogger(logger corazawaf.ErrorLogCallback) WAFConfig
+	// WithErrorCallback configures an error callback that can be used
+	// to log errors triggered by the WAF.
+	// It contains the severity so the cb can decide to skip it or not
+	WithErrorCallback(logger func(rule types.MatchedRule)) WAFConfig
 
 	// WithRootFS configures the root file system.
 	WithRootFS(fs fs.FS) WAFConfig
@@ -110,7 +112,7 @@ type wafConfig struct {
 	requestBody      *requestBodyConfig
 	responseBody     *responseBodyConfig
 	debugLogger      loggers.DebugLogger
-	errorLogger      corazawaf.ErrorLogCallback
+	errorCallback    func(rule types.MatchedRule)
 	fsRoot           fs.FS
 }
 
@@ -168,9 +170,9 @@ func (c *wafConfig) WithDebugLogger(logger loggers.DebugLogger) WAFConfig {
 	return ret
 }
 
-func (c *wafConfig) WithErrorLogger(logger corazawaf.ErrorLogCallback) WAFConfig {
+func (c *wafConfig) WithErrorCallback(logger func(rule types.MatchedRule)) WAFConfig {
 	ret := c.clone()
-	ret.errorLogger = logger
+	ret.errorCallback = logger
 	return ret
 }
 
diff --git a/examples/http-server/main.go b/examples/http-server/main.go
index fb5fc6a..4325ec7 100644
--- a/examples/http-server/main.go
+++ b/examples/http-server/main.go
@@ -47,7 +47,7 @@ func createWAF() coraza.WAF {
 
 	waf, err := coraza.NewWAF(
 		coraza.NewWAFConfig().
-			WithErrorLogger(logError).
+			WithErrorCallback(logError).
 			WithDirectivesFromFile(directivesFile),
 	)
 	if err != nil {
diff --git a/http/middleware.go b/http/middleware.go
index 6ad5639..23e83a3 100644
--- a/http/middleware.go
+++ b/http/middleware.go
@@ -17,10 +17,6 @@ import (
 	"github.com/corazawaf/coraza/v3/types"
 )
 
-type nopCloser struct{}
-
-func (nopCloser) Close() error { return nil }
-
 // processRequest fills all transaction variables from an http.Request object
 // Most implementations of Coraza will probably use http.Request objects
 // so this will implement all phase 0, 1 and 2 variables
@@ -64,14 +60,14 @@ func processRequest(tx types.Transaction, req *http.Request) (*types.Interruptio
 		if req.Body != nil && req.Body != http.NoBody {
 			_, err := io.Copy(tx.RequestBodyWriter(), req.Body)
 			if err != nil {
-				return tx.Interruption(), err
+				return nil, err
 			}
-			_ = req.Body.Close()
 
 			reader, err := tx.RequestBodyReader()
 			if err != nil {
-				return tx.Interruption(), err
+				return nil, err
 			}
+			reader = io.MultiReader(reader, req.Body)
 			// req.Body is transparently reinizialied with a new io.ReadCloser.
 			// The http handler will be able to read it.
 			// Prior to Go 1.19 NopCloser does not implement WriterTo if the reader implements it.
@@ -85,12 +81,12 @@ func processRequest(tx types.Transaction, req *http.Request) (*types.Interruptio
 					io.Reader
 					io.WriterTo
 					io.Closer
-				}{reader, rwt, nopCloser{}}
+				}{reader, rwt, req.Body}
 			} else {
 				req.Body = struct {
 					io.Reader
 					io.Closer
-				}{reader, nopCloser{}}
+				}{reader, req.Body}
 			}
 		}
 
@@ -101,7 +97,6 @@ func processRequest(tx types.Transaction, req *http.Request) (*types.Interruptio
 }
 
 func WrapHandler(waf coraza.WAF, l Logger, h http.Handler) http.Handler {
-
 	fn := func(w http.ResponseWriter, r *http.Request) {
 		tx := waf.NewTransaction()
 		defer func() {
diff --git a/internal/corazarules/rule_match.go b/internal/corazarules/rule_match.go
index 194c5b9..70457fa 100644
--- a/internal/corazarules/rule_match.go
+++ b/internal/corazarules/rule_match.go
@@ -120,7 +120,6 @@ func (mr *MatchedRule) Rule() types.RuleMetadata {
 func (mr MatchedRule) details(matchData types.MatchData) string {
 	log := &strings.Builder{}
 
-	resolvedIP := ""
 	msg := matchData.Message()
 	data := matchData.Data()
 	if len(msg) > 200 {
@@ -136,7 +135,7 @@ func (mr MatchedRule) details(matchData types.MatchData) string {
 		log.WriteString(fmt.Sprintf(" [tag %q]", t))
 	}
 	log.WriteString(fmt.Sprintf(" [hostname %q] [uri %q] [unique_id %q]",
-		resolvedIP, mr.URI_, mr.TransactionID_))
+		mr.ServerIPAddress_, mr.URI_, mr.TransactionID_))
 	return log.String()
 }
 
diff --git a/internal/corazawaf/body_buffer.go b/internal/corazawaf/body_buffer.go
index 1e40d1e..1a476a9 100644
--- a/internal/corazawaf/body_buffer.go
+++ b/internal/corazawaf/body_buffer.go
@@ -60,15 +60,36 @@ func (br *BodyBuffer) Write(data []byte) (n int, err error) {
 	return br.buffer.Write(data)
 }
 
+type bodyBufferReader struct {
+	pos int
+	br  *BodyBuffer
+}
+
+func (b *bodyBufferReader) Read(p []byte) (n int, err error) {
+	if environment.IsTinyGo || b.br.writer == nil {
+		buf := b.br.buffer.Bytes()
+		n = len(p)
+		if b.pos+n > len(buf) {
+			n = len(buf) - b.pos
+		}
+		if n == 0 {
+			return 0, io.EOF
+		}
+		copy(p, buf[b.pos:b.pos+n])
+		b.pos += n
+		return
+	}
+
+	n, err = b.br.writer.ReadAt(p, int64(b.pos))
+	b.pos += n
+	return
+}
+
 // Reader Returns a working reader for the body buffer in memory or file
 func (br *BodyBuffer) Reader() (io.Reader, error) {
-	if environment.IsTinyGo || br.writer == nil {
-		return bytes.NewReader(br.buffer.Bytes()), nil
-	}
-	if _, err := br.writer.Seek(0, 0); err != nil {
-		return nil, err
-	}
-	return br.writer, nil
+	return &bodyBufferReader{
+		br: br,
+	}, nil
 }
 
 // Size returns the current size of the body buffer
diff --git a/internal/corazawaf/transaction.go b/internal/corazawaf/transaction.go
index 324aa45..71aed0a 100644
--- a/internal/corazawaf/transaction.go
+++ b/internal/corazawaf/transaction.go
@@ -1077,7 +1077,7 @@ func (tx *Transaction) AuditLog() *loggers.AuditLog {
 // This method helps the GC to clean up the transaction faster and release resources
 // It also allows caches the transaction back into the sync.Pool
 func (tx *Transaction) Close() error {
-	defer transactionPool.Put(tx)
+	defer tx.WAF.txPool.Put(tx)
 	tx.variables.reset()
 	var errs []error
 	if err := tx.RequestBodyBuffer.Reset(); err != nil {
diff --git a/internal/corazawaf/waf.go b/internal/corazawaf/waf.go
index 88bb575..54531ca 100644
--- a/internal/corazawaf/waf.go
+++ b/internal/corazawaf/waf.go
@@ -21,14 +21,6 @@ import (
 	"github.com/corazawaf/coraza/v3/types"
 )
 
-// Initializing pool for transactions
-var transactionPool = sync.NewPool(func() interface{} { return new(Transaction) })
-
-// ErrorLogCallback is used to set a callback function to log errors
-// It is triggered when an error is raised by the WAF
-// It contains the severity so the cb can decide to log it or not
-type ErrorLogCallback = func(rule types.MatchedRule)
-
 // WAF instance is used to store configurations and rules
 // Every web application should have a different WAF instance,
 // but you can share an instance if you are ok with sharing
@@ -39,6 +31,8 @@ type ErrorLogCallback = func(rule types.MatchedRule)
 // All WAF instance fields are immutable, if you update any
 // of them in runtime you might create concurrency issues
 type WAF struct {
+	txPool sync.Pool
+
 	// ruleGroup object, contains all rules and helpers
 	Rules RuleGroup
 
@@ -129,7 +123,7 @@ type WAF struct {
 	// Used for the debug logger
 	Logger loggers.DebugLogger
 
-	ErrorLogCb ErrorLogCallback
+	ErrorLogCb func(rule types.MatchedRule)
 
 	// AuditLogWriter is used to write audit logs
 	AuditLogWriter loggers.LogWriter
@@ -151,7 +145,7 @@ func (w *WAF) NewTransactionWithID(id string) *Transaction {
 // NewTransactionWithID Creates a new initialized transaction for this WAF instance
 // Using the specified ID
 func (w *WAF) newTransactionWithID(id string) *Transaction {
-	tx := transactionPool.Get().(*Transaction)
+	tx := w.txPool.Get().(*Transaction)
 	tx.id = id
 	tx.matchedRules = []types.MatchedRule{}
 	tx.interruption = nil
@@ -270,6 +264,8 @@ func NewWAF() *WAF {
 		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,
@@ -304,9 +300,9 @@ func (w *WAF) SetDebugLogLevel(lvl int) error {
 	return nil
 }
 
-// SetErrorLogCb sets the callback function for error logging
+// SetErrorCallback sets the callback function for error logging
 // The error callback receives all the error data and some
 // helpers to write modsecurity style logs
-func (w *WAF) SetErrorLogCb(cb ErrorLogCallback) {
+func (w *WAF) SetErrorCallback(cb func(rule types.MatchedRule)) {
 	w.ErrorLogCb = cb
 }
diff --git a/magefile.go b/magefile.go
index 2304ed3..593e7ae 100644
--- a/magefile.go
+++ b/magefile.go
@@ -101,6 +101,23 @@ func Coverage() error {
 	return sh.RunV("go", "tool", "cover", "-html=build/coverage.txt", "-o", "build/coverage.html")
 }
 
+// Fuzz runs fuzz tests
+func Fuzz() error {
+	// Go must be run once per test when fuzzing
+	transformationTests := []string{
+		"FuzzB64Decode",
+		"FuzzCMDLine",
+	}
+	for _, test := range transformationTests {
+		fmt.Println("Running", test)
+		if err := sh.RunV("go", "test", "-fuzz="+test, "-fuzztime=2m", "./transformations"); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
 // Doc runs godoc, access at http://localhost:6060
 func Doc() error {
 	return sh.RunV("go", "run", "golang.org/x/tools/cmd/godoc@latest", "-http=:6060")
diff --git a/transformations/escape_seq_decode.go b/transformations/escape_seq_decode.go
index e4ec15b..5178d8b 100644
--- a/transformations/escape_seq_decode.go
+++ b/transformations/escape_seq_decode.go
@@ -5,18 +5,30 @@ package transformations
 
 import (
 	"strconv"
+	"strings"
 
 	utils "github.com/corazawaf/coraza/v3/internal/strings"
 )
 
 func escapeSeqDecode(input string) (string, error) {
-	var i, count, d int
+	if i := strings.IndexByte(input, '\\'); i != -1 {
+		// TODO: This will transform even if the backslash isn't followed by an escape,
+		// but keep it simple for now.
+		return doEscapeSeqDecode(input, i), nil
+	}
+	return input, nil
+}
+
+func doEscapeSeqDecode(input string, pos int) string {
 	inputLen := len(input)
 	data := []byte(input)
 
+	d := pos
+	i := pos
+
 	for i < inputLen {
 		if (input[i] == '\\') && (i+1 < inputLen) {
-			c := -1
+			c := int8(-1)
 
 			switch input[i+1] {
 			case 'a':
@@ -53,7 +65,7 @@ func escapeSeqDecode(input string) (string, error) {
 					/* Hexadecimal. */
 					if (i+3 < inputLen) && (utils.ValidHex((input[i+2]))) && (utils.ValidHex((input[i+3]))) {
 						/* Two digits. */
-						c = int(utils.X2c(input[i+2:]))
+						c = int8(utils.X2c(input[i+2:]))
 						i += 4
 					}
 					/* Else Invalid encoding, do nothing. */
@@ -75,8 +87,8 @@ func escapeSeqDecode(input string) (string, error) {
 						buf = buf[:j]
 
 						if j > 0 {
-							bc, _ := strconv.ParseInt(string(buf), 8, 32)
-							c = int(bc)
+							bc, _ := strconv.ParseUint(string(buf), 8, 8)
+							c = int8(bc)
 							i += 1 + j
 						}
 					}
@@ -87,23 +99,20 @@ func escapeSeqDecode(input string) (string, error) {
 				/* Didn't recognise encoding, copy raw bytes. */
 				data[d] = input[i+1]
 				d++
-				count++
 				i += 2
 			} else {
 				/* Converted the encoding. */
 				data[d] = byte(c)
 				d++
-				count++
 			}
 		} else {
 			/* Input character not a backslash, copy it. */
 			data[d] = input[i]
 			d++
 			i++
-			count++
 		}
 	}
-	return string(data[:count]), nil
+	return utils.WrapUnsafe(data[:d])
 }
 
 func isODigit(c byte) bool {
diff --git a/transformations/js_decode.go b/transformations/js_decode.go
index b2ea9c2..4cd7c3a 100644
--- a/transformations/js_decode.go
+++ b/transformations/js_decode.go
@@ -5,19 +5,27 @@ package transformations
 
 import (
 	"strconv"
+	"strings"
 
 	utils "github.com/corazawaf/coraza/v3/internal/strings"
 )
 
 func jsDecode(data string) (string, error) {
-	//https://github.com/SpiderLabs/ModSecurity/blob/b66224853b4e9d30e0a44d16b29d5ed3842a6b11/src/actions/transformations/js_decode.cc
-	return doJsDecode(data), nil
+	if i := strings.IndexByte(data, '\\'); i != -1 {
+		// TODO: This will transform even if the backslash isn't followed by an escape,
+		// but keep it simple for now.
+		return doJsDecode(data, i), nil
+	}
+	return data, nil
 }
 
-func doJsDecode(input string) string {
+// https://github.com/SpiderLabs/ModSecurity/blob/b66224853b4e9d30e0a44d16b29d5ed3842a6b11/src/actions/transformations/js_decode.cc
+func doJsDecode(input string, pos int) string {
 	d := []byte(input)
 	inputLen := len(input)
-	var i, c int
+
+	i := pos
+	c := pos
 
 	for i < inputLen {
 		if input[i] == '\\' {
@@ -109,7 +117,7 @@ func doJsDecode(input string) string {
 		}
 	}
 
-	return string(d[:c])
+	return utils.WrapUnsafe(d[:c])
 
 }
 
diff --git a/transformations/remove_comments.go b/transformations/remove_comments.go
index 8d0a6c7..0e8f1bc 100644
--- a/transformations/remove_comments.go
+++ b/transformations/remove_comments.go
@@ -18,13 +18,13 @@ charLoop:
 			case (input[i] == '/') && (i+1 < inputLen) && (input[i+1] == '*'):
 				incomment = true
 				i += 2
-			case (input[i] == '<') && (i+1 < inputLen) && (input[i+1] == '!') && (i+2 < inputLen) && (input[i+2] == '-') && (i+3 < inputLen) && (input[i+3] == '-') && !incomment:
+			case (input[i] == '<') && (i+3 < inputLen) && (input[i+1] == '!') && (input[i+2] == '-') && (input[i+3] == '-'):
 				incomment = true
 				i += 4
-			case (input[i] == '-') && (i+1 < inputLen) && (input[i+1] == '-') && !incomment:
+			case (input[i] == '-') && (i+1 < inputLen) && (input[i+1] == '-'):
 				input[i] = ' '
 				break charLoop
-			case input[i] == '#' && !incomment:
+			case input[i] == '#':
 				input[i] = ' '
 				break charLoop
 			default:
@@ -40,7 +40,7 @@ charLoop:
 				input[j] = input[i]
 				i++
 				j++
-			case (input[i] == '-') && (i+1 < inputLen) && (input[i+1] == '-') && (i+2 < inputLen) && (input[i+2] == '>'):
+			case (input[i] == '-') && (i+2 < inputLen) && (input[i+1] == '-') && (input[i+2] == '>'):
 				incomment = false
 				i += 3
 				input[j] = input[i]
diff --git a/waf.go b/waf.go
index c630428..9eb7572 100644
--- a/waf.go
+++ b/waf.go
@@ -91,8 +91,8 @@ func NewWAF(config WAFConfig) (WAF, error) {
 		waf.ResponseBodyLimit = int64(r.limit)
 	}
 
-	if c.errorLogger != nil {
-		waf.ErrorLogCb = c.errorLogger
+	if c.errorCallback != nil {
+		waf.ErrorLogCb = c.errorCallback
 	}
 
 	return wafWrapper{waf: waf}, nil


Test patch:
diff --git a/bodyprocessors/multipart_test.go b/bodyprocessors/multipart_test.go
index 4801a64..446f35c 100644
--- a/bodyprocessors/multipart_test.go
+++ b/bodyprocessors/multipart_test.go
@@ -11,6 +11,27 @@ import (
 	"github.com/corazawaf/coraza/v3/internal/corazawaf"
 )
 
+func multipartProcessor(t *testing.T) bodyprocessors.BodyProcessor {
+	t.Helper()
+	mp, err := bodyprocessors.Get("multipart")
+	if err != nil {
+		t.Fatal(err)
+	}
+	return mp
+}
+
+func TestProcessRequestFailsDueToIncorrectMimeType(t *testing.T) {
+	mp := multipartProcessor(t)
+
+	expectedError := "not a multipart body"
+
+	if err := mp.ProcessRequest(strings.NewReader(""), corazawaf.NewTransactionVariables(), bodyprocessors.Options{
+		Mime: "application/json",
+	}); err == nil || err.Error() != expectedError {
+		t.Fatal("expected error")
+	}
+}
+
 func TestMultipartPayload(t *testing.T) {
 	payload := strings.TrimSpace(`
 -----------------------------9051914041544843365972754266
@@ -31,10 +52,9 @@ Content-Type: text/html
 
 -----------------------------9051914041544843365972754266--
 `)
-	mp, err := bodyprocessors.Get("multipart")
-	if err != nil {
-		t.Fatal(err)
-	}
+
+	mp := multipartProcessor(t)
+
 	v := corazawaf.NewTransactionVariables()
 	if err := mp.ProcessRequest(strings.NewReader(payload), v, bodyprocessors.Options{
 		Mime: "multipart/form-data; boundary=---------------------------9051914041544843365972754266",
diff --git a/http/middleware_test.go b/http/middleware_test.go
index 1d8e690..bafe8f8 100644
--- a/http/middleware_test.go
+++ b/http/middleware_test.go
@@ -10,12 +10,12 @@ package http
 import (
 	"bufio"
 	"bytes"
+	"fmt"
 	"io"
 	"mime/multipart"
 	"net/http"
 	"net/http/httptest"
-	"os"
-	"path/filepath"
+	"net/textproto"
 	"strconv"
 	"strings"
 	"testing"
@@ -60,77 +60,68 @@ func TestProcessRequestEngineOff(t *testing.T) {
 }
 
 func TestProcessRequestMultipart(t *testing.T) {
-	req, _ := http.NewRequest("POST", "/some", nil)
-	if err := multipartRequest(t, req); err != nil {
-		t.Fatal(err)
-	}
-	tx := makeTransaction(t)
+	waf := corazawaf.NewWAF()
+	waf.RequestBodyAccess = true
+
+	tx := waf.NewTransaction()
 	tx.RequestBodyAccess = true
+
+	req := createMultipartRequest(t)
+
 	if _, err := processRequest(tx, req); err != nil {
 		t.Fatal(err)
 	}
+
 	if req.Body == nil {
-		t.Error("failed to process multipart request")
+		t.Error("failed to process multipart request: nil body")
 	}
 	defer req.Body.Close()
 
 	reader := bufio.NewReader(req.Body)
 	if _, err := reader.ReadString('\n'); err != nil {
-		t.Error("failed to read multipart request", err)
+		t.Errorf("failed to read multipart request: %s", err.Error())
 	}
+
 	if err := tx.Close(); err != nil {
-		t.Error(err)
+		t.Errorf("failed to close the transaction: %s", err.Error())
 	}
 }
 
-func multipartRequest(t *testing.T, req *http.Request) error {
+func createMultipartRequest(t *testing.T) *http.Request {
 	t.Helper()
 
-	var b bytes.Buffer
-	w := multipart.NewWriter(&b)
-	tempfile, err := os.Create(filepath.Join(t.TempDir(), "tmpfile"))
+	metadata := `{"name": "photo-sample.jpeg"}`
+	body := &bytes.Buffer{}
+	writer := multipart.NewWriter(body)
+	metadataHeader := textproto.MIMEHeader{}
+	metadataHeader.Set("Content-Type", "application/json; charset=UTF-8")
+
+	part, err := writer.CreatePart(metadataHeader)
 	if err != nil {
-		return err
-	}
-	for i := 0; i < 1024*5; i++ {
-		// this should create a 5mb file
-		if _, err := tempfile.Write([]byte(strings.Repeat("A", 1024))); err != nil {
-			return err
-		}
-	}
-	var fw io.Writer
-	if fw, err = w.CreateFormFile("fupload", tempfile.Name()); err != nil {
-		return err
-	}
-	if _, err := tempfile.Seek(0, 0); err != nil {
-		return err
+		t.Fatal(err)
 	}
-	if _, err = io.Copy(fw, tempfile); err != nil {
-		return err
+	_, _ = part.Write([]byte(metadata))
+
+	mediaHeader := textproto.MIMEHeader{}
+	mediaHeader.Set("Content-Type", "image/jpeg")
+
+	mediaPart, err := writer.CreatePart(mediaHeader)
+	if err != nil {
+		t.Fatal(err)
 	}
-	req.Body = io.NopCloser(&b)
-	req.Header.Set("Content-Type", w.FormDataContentType())
-	req.Method = "POST"
-	return nil
-}
+	_, _ = io.Copy(mediaPart, bytes.NewReader([]byte{255, 1, 2}))
 
-func makeTransaction(t *testing.T) *corazawaf.Transaction {
-	t.Helper()
-	tx := corazawaf.NewWAF().NewTransaction()
-	tx.RequestBodyAccess = true
-	ht := []string{
-		"POST /testurl.php?id=123&b=456 HTTP/1.1",
-		"Host: www.test.com:80",
-		"Cookie: test=123",
-		"Content-Type: application/x-www-form-urlencoded",
-		"X-Test-Header: test456",
-		"Content-Length: 13",
-		"",
-		"testfield=456",
+	writer.Close()
+
+	req, err := http.NewRequest("POST", "/some", body)
+	if err != nil {
+		t.Fatal(err)
 	}
-	data := strings.Join(ht, "\r\n")
-	_, _ = tx.ParseRequestReader(strings.NewReader(data))
-	return tx
+
+	req.Header.Set("Content-Type", writer.FormDataContentType())
+	req.Header.Set("Content-Length", fmt.Sprintf("%d", body.Len()))
+
+	return req
 }
 
 // from issue https://github.com/corazawaf/coraza/issues/159 @zpeasystart
@@ -224,30 +215,12 @@ func (l *debugLogger) SetOutput(w io.WriteCloser) {
 	l.t.Log("ignoring SecDebugLog directive, debug logs are always routed to proxy logs")
 }
 
-func createWAF(t *testing.T) coraza.WAF {
-	t.Helper()
-	waf, err := coraza.NewWAF(coraza.NewWAFConfig().
-		WithDirectives(`
-		# This is a comment
-		SecDebugLogLevel 5
-		SecRequestBodyAccess On
-		SecResponseBodyAccess On
-		SecResponseBodyMimeType text/plain
-		SecRule ARGS:id "@eq 0" "id:1, phase:1,deny, status:403,msg:'Invalid id',log,auditlog"
-		SecRule REQUEST_BODY "@contains eval" "id:100, phase:2,deny, status:403,msg:'Invalid request body',log,auditlog"
-		SecRule RESPONSE_HEADERS:Foo "@pm bar" "id:199,phase:3,deny,t:lowercase,deny, status:401,msg:'Invalid response header',log,auditlog"
-		SecRule RESPONSE_BODY "@contains password" "id:200, phase:4,deny, status:403,msg:'Invalid response body',log,auditlog"
-	`).WithErrorLogger(errLogger(t)).WithDebugLogger(&debugLogger{t: t}))
-	if err != nil {
-		t.Fatal(err)
-	}
-	return waf
-}
-
 type httpTest struct {
 	http2            bool
 	reqURI           string
 	reqBody          string
+	echoReqBody      bool
+	reqBodyLimit     int
 	respHeaders      map[string]string
 	respBody         string
 	expectedProto    string
@@ -279,6 +252,16 @@ func TestHttpServer(t *testing.T) {
 			expectedProto:  "HTTP/1.1",
 			expectedStatus: 403,
 		},
+		"request body larger than limit": {
+			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')",
+		},
 		"response headers blocking": {
 			reqURI:         "/hello",
 			respHeaders:    map[string]string{"foo": "bar"},
@@ -304,8 +287,26 @@ func TestHttpServer(t *testing.T) {
 	// Perform tests
 	for name, tCase := range tests {
 		t.Run(name, func(t *testing.T) {
-			runAgainstWaf(t, tCase, createWAF(t))
-
+			conf := coraza.NewWAFConfig().
+				WithDirectives(`
+		# This is a comment
+		SecDebugLogLevel 5
+		SecRequestBodyAccess On
+		SecResponseBodyAccess On
+		SecResponseBodyMimeType text/plain
+		SecRule ARGS:id "@eq 0" "id:1, phase:1,deny, status:403,msg:'Invalid id',log,auditlog"
+		SecRule REQUEST_BODY "@contains eval" "id:100, phase:2,deny, status:403,msg:'Invalid request body',log,auditlog"
+		SecRule RESPONSE_HEADERS:Foo "@pm bar" "id:199,phase:3,deny,t:lowercase,deny, status:401,msg:'Invalid response header',log,auditlog"
+		SecRule RESPONSE_BODY "@contains password" "id:200, phase:4,deny, status:403,msg:'Invalid response body',log,auditlog"
+	`).WithErrorCallback(errLogger(t)).WithDebugLogger(&debugLogger{t: t})
+			if l := tCase.reqBodyLimit; l > 0 {
+				conf = conf.WithRequestBodyAccess(coraza.NewRequestBodyConfig().WithLimit(l).WithInMemoryLimit(l))
+			}
+			waf, err := coraza.NewWAF(conf)
+			if err != nil {
+				t.Fatal(err)
+			}
+			runAgainstWaf(t, tCase, waf)
 		})
 	}
 }
@@ -344,7 +345,7 @@ func TestHttpServerWithRuleEngineOff(t *testing.T) {
 			SecRequestBodyAccess On
 			SecRule ARGS:id "@eq 0" "id:1, phase:1,deny, status:403,msg:'Invalid id',log,auditlog"
 			SecRule REQUEST_BODY "@contains eval" "id:100, phase:2,deny, status:403,msg:'Invalid request body',log,auditlog"
-			`).WithErrorLogger(errLogger(t)).WithDebugLogger(&debugLogger{t: t}))
+			`).WithErrorCallback(errLogger(t)).WithDebugLogger(&debugLogger{t: t}))
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -370,9 +371,18 @@ func runAgainstWaf(t *testing.T, tCase httpTest, waf coraza.WAF) {
 			w.Header().Set(k, v)
 		}
 		w.WriteHeader(201)
-		_, err := w.Write([]byte(tCase.respBody))
-		if err != nil {
-			serverErrC <- err
+		if tCase.echoReqBody {
+			buf, err := io.ReadAll(req.Body)
+			if err != nil {
+				serverErrC <- err
+			}
+			if _, err := w.Write(buf); err != nil {
+				serverErrC <- err
+			}
+		} else {
+			if _, err := w.Write([]byte(tCase.respBody)); err != nil {
+				serverErrC <- err
+			}
 		}
 	})))
 	if tCase.http2 {
diff --git a/internal/corazawaf/transaction_test.go b/internal/corazawaf/transaction_test.go
index c826b71..ab95f20 100644
--- a/internal/corazawaf/transaction_test.go
+++ b/internal/corazawaf/transaction_test.go
@@ -19,8 +19,6 @@ import (
 	"github.com/corazawaf/coraza/v3/types/variables"
 )
 
-var wafi = NewWAF()
-
 func TestTxSettersMultipart(t *testing.T) {
 	tx := makeTransactionMultipart(t)
 	exp := map[string]string{
@@ -64,7 +62,7 @@ func TestTxSetters(t *testing.T) {
 	validateMacroExpansion(exp, tx, t)
 }
 func TestTxMultipart(t *testing.T) {
-	tx := wafi.NewTransaction()
+	tx := NewWAF().NewTransaction()
 	body := []string{
 		"-----------------------------9051914041544843365972754266",
 		"Content-Disposition: form-data; name=\"text\"",
@@ -109,7 +107,7 @@ func TestTxMultipart(t *testing.T) {
 
 func TestTxResponse(t *testing.T) {
 	/*
-		tx := wafi.NewTransaction()
+		tx := NewWAF().NewTransaction()
 		ht := []string{
 			"HTTP/1.1 200 OK",
 			"Content-Type: text/html",
@@ -167,7 +165,7 @@ func TestRequestBody(t *testing.T) {
 		t.Run(testCase.name, func(t *testing.T) {
 			urlencoded := "some=result&second=data"
 			// xml := "<test><content>test</content></test>"
-			tx := wafi.NewTransaction()
+			tx := NewWAF().NewTransaction()
 			tx.RequestBodyAccess = true
 			tx.RequestBodyLimit = testCase.requestBodyLimit
 			tx.WAF.RequestBodyLimitAction = testCase.requestBodyLimitAction
@@ -211,7 +209,7 @@ func TestResponseHeader(t *testing.T) {
 }
 
 func TestProcessRequestHeadersDoesNoEvaluationOnEngineOff(t *testing.T) {
-	tx := wafi.NewTransaction()
+	tx := NewWAF().NewTransaction()
 	tx.RuleEngine = types.RuleEngineOff
 
 	if !tx.IsRuleEngineOff() {
@@ -225,7 +223,7 @@ func TestProcessRequestHeadersDoesNoEvaluationOnEngineOff(t *testing.T) {
 }
 
 func TestProcessRequestBodyDoesNoEvaluationOnEngineOff(t *testing.T) {
-	tx := wafi.NewTransaction()
+	tx := NewWAF().NewTransaction()
 	tx.RuleEngine = types.RuleEngineOff
 	if _, err := tx.ProcessRequestBody(); err != nil {
 		t.Error("failed to process request body")
@@ -236,7 +234,7 @@ func TestProcessRequestBodyDoesNoEvaluationOnEngineOff(t *testing.T) {
 }
 
 func TestProcessResponseHeadersDoesNoEvaluationOnEngineOff(t *testing.T) {
-	tx := wafi.NewTransaction()
+	tx := NewWAF().NewTransaction()
 	tx.RuleEngine = types.RuleEngineOff
 	_ = tx.ProcessResponseHeaders(200, "OK")
 	if tx.LastPhase != 0 {
@@ -245,7 +243,7 @@ func TestProcessResponseHeadersDoesNoEvaluationOnEngineOff(t *testing.T) {
 }
 
 func TestProcessResponseBodyDoesNoEvaluationOnEngineOff(t *testing.T) {
-	tx := wafi.NewTransaction()
+	tx := NewWAF().NewTransaction()
 	tx.RuleEngine = types.RuleEngineOff
 	if _, err := tx.ProcessResponseBody(); err != nil {
 		t.Error("Failed to process response body")
@@ -256,7 +254,7 @@ func TestProcessResponseBodyDoesNoEvaluationOnEngineOff(t *testing.T) {
 }
 
 func TestProcessLoggingDoesNoEvaluationOnEngineOff(t *testing.T) {
-	tx := wafi.NewTransaction()
+	tx := NewWAF().NewTransaction()
 	tx.RuleEngine = types.RuleEngineOff
 	tx.ProcessLogging()
 	if tx.LastPhase != 0 {
@@ -359,7 +357,7 @@ func TestRelevantAuditLogging(t *testing.T) {
 func TestLogCallback(t *testing.T) {
 	waf := NewWAF()
 	buffer := ""
-	waf.SetErrorLogCb(func(mr types.MatchedRule) {
+	waf.SetErrorCallback(func(mr types.MatchedRule) {
 		buffer = mr.ErrorLog(403)
 	})
 	tx := waf.NewTransaction()
@@ -666,7 +664,7 @@ func BenchmarkTransactionCreation(b *testing.B) {
 
 func makeTransaction(t testing.TB) *Transaction {
 	t.Helper()
-	tx := wafi.NewTransaction()
+	tx := NewWAF().NewTransaction()
 	tx.RequestBodyAccess = true
 	ht := []string{
 		"POST /testurl.php?id=123&b=456 HTTP/1.1",
@@ -690,7 +688,7 @@ func makeTransactionMultipart(t *testing.T) *Transaction {
 	if t != nil {
 		t.Helper()
 	}
-	tx := wafi.NewTransaction()
+	tx := NewWAF().NewTransaction()
 	tx.RequestBodyAccess = true
 	ht := []string{
 		"POST /testurl.php?id=123&b=456 HTTP/1.1",
diff --git a/internal/seclang/rules_test.go b/internal/seclang/rules_test.go
index 61099eb..d2474bf 100644
--- a/internal/seclang/rules_test.go
+++ b/internal/seclang/rules_test.go
@@ -131,7 +131,7 @@ func TestSecAuditLogs(t *testing.T) {
 func TestRuleLogging(t *testing.T) {
 	waf := corazawaf.NewWAF()
 	var logs []string
-	waf.SetErrorLogCb(func(mr types.MatchedRule) {
+	waf.SetErrorCallback(func(mr types.MatchedRule) {
 		logs = append(logs, mr.ErrorLog(403))
 	})
 	parser := NewParser(waf)
@@ -192,7 +192,7 @@ func TestRuleChains(t *testing.T) {
 func TestTagsAreNotPrintedTwice(t *testing.T) {
 	waf := corazawaf.NewWAF()
 	var logs []string
-	waf.SetErrorLogCb(func(mr types.MatchedRule) {
+	waf.SetErrorCallback(func(mr types.MatchedRule) {
 		logs = append(logs, mr.ErrorLog(403))
 	})
 	parser := NewParser(waf)
@@ -224,7 +224,7 @@ func TestTagsAreNotPrintedTwice(t *testing.T) {
 func TestStatusFromInterruptions(t *testing.T) {
 	waf := corazawaf.NewWAF()
 	var logs []string
-	waf.SetErrorLogCb(func(mr types.MatchedRule) {
+	waf.SetErrorCallback(func(mr types.MatchedRule) {
 		logs = append(logs, mr.ErrorLog(403))
 	})
 	parser := NewParser(waf)
@@ -262,7 +262,7 @@ func TestChainWithUnconditionalMatch(t *testing.T) {
 func TestLogsAreNotPrintedManyTimes(t *testing.T) {
 	waf := corazawaf.NewWAF()
 	var logs []string
-	waf.SetErrorLogCb(func(mr types.MatchedRule) {
+	waf.SetErrorCallback(func(mr types.MatchedRule) {
 		logs = append(logs, mr.ErrorLog(403))
 	})
 	parser := NewParser(waf)
diff --git a/testing/coreruleset/coreruleset_test.go b/testing/coreruleset/coreruleset_test.go
index e333184..23d40d5 100644
--- a/testing/coreruleset/coreruleset_test.go
+++ b/testing/coreruleset/coreruleset_test.go
@@ -217,7 +217,7 @@ SecRule REQUEST_HEADERS:X-CRS-Test "@rx ^.*$" \
 		t.Fatalf("failed to create error log: %v", err)
 	}
 	errorWriter := bufio.NewWriter(errorFile)
-	conf = conf.WithErrorLogger(func(rule types.MatchedRule) {
+	conf = conf.WithErrorCallback(func(rule types.MatchedRule) {
 		msg := rule.ErrorLog(0)
 		if _, err := io.WriteString(errorWriter, msg); err != nil {
 			t.Fatal(err)
@@ -237,10 +237,10 @@ SecRule REQUEST_HEADERS:X-CRS-Test "@rx ^.*$" \
 		w.Header().Set("Content-Type", "text/plain")
 		switch {
 		case r.URL.Path == "/anything":
+			body, err := io.ReadAll(r.Body)
 			// Emulated httpbin behaviour: /anything endpoint acts as an echo server, writing back the request body
 			if r.Header.Get("Content-Type") == "application/x-www-form-urlencoded" {
 				// Tests 954120-1 and 954120-2 are the only two calling /anything with a POST and payload is urlencoded
-				body, _ := io.ReadAll(r.Body)
 				if err != nil {
 					t.Fatalf("handler can not read request body: %v", err)
 				}
@@ -250,7 +250,7 @@ SecRule REQUEST_HEADERS:X-CRS-Test "@rx ^.*$" \
 				}
 				fmt.Fprintf(w, urldecodedBody)
 			} else {
-				_, err = io.Copy(w, r.Body)
+				_, err = w.Write(body)
 			}
 
 		case strings.HasPrefix(r.URL.Path, "/base64/"):
diff --git a/transformations/base64decode_test.go b/transformations/base64decode_test.go
index eb39dfd..2f2a74b 100644
--- a/transformations/base64decode_test.go
+++ b/transformations/base64decode_test.go
@@ -3,16 +3,21 @@
 
 package transformations
 
-import "testing"
+import (
+	"bytes"
+	"encoding/base64"
+	"strings"
+	"testing"
+)
 
-func BenchmarkB64Decode(b *testing.B) {
-	tests := []string{
-		"VGVzdENhc2U=",
-		"P.HNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==",
-		"VGVzdABDYXNl",
-	}
+var b64DecodeTests = []string{
+	"VGVzdENhc2U=",
+	"P.HNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==",
+	"VGVzdABDYXNl",
+}
 
-	for _, tt := range tests {
+func BenchmarkB64Decode(b *testing.B) {
+	for _, tt := range b64DecodeTests {
 		b.Run(tt, func(b *testing.B) {
 			for i := 0; i < b.N; i++ {
 				_, err := base64decode(tt)
@@ -23,3 +28,24 @@ func BenchmarkB64Decode(b *testing.B) {
 		})
 	}
 }
+
+func FuzzB64Decode(f *testing.F) {
+	for _, tc := range b64DecodeTests {
+		f.Add(tc)
+	}
+	f.Fuzz(func(t *testing.T, tc string) {
+		data, err := base64decode(tc)
+		// We decode base64 within non-base64 so there is no
+		// error case.
+		if err != nil {
+			t.Error(err)
+		}
+
+		refData, err := base64.StdEncoding.DecodeString(tc)
+		// The standard library decoder will fail on many inputs ours succeeds on, but when
+		// it doesn't and there are no newlines in the input, they should match.
+		if err == nil && !strings.ContainsAny(tc, "\n\r") && !bytes.Equal([]byte(data), refData) {
+			t.Errorf("mismatch with stdlib for input %s", tc)
+		}
+	})
+}
diff --git a/transformations/cmd_line_test.go b/transformations/cmd_line_test.go
index 00894bf..e7117fd 100644
--- a/transformations/cmd_line_test.go
+++ b/transformations/cmd_line_test.go
@@ -3,17 +3,20 @@
 
 package transformations
 
-import "testing"
+import (
+	"strings"
+	"testing"
+)
 
-func BenchmarkCMDLine(b *testing.B) {
-	tests := []string{
-		"",
-		"test",
-		"C^OMMAND /C DIR",
-		"\"command\" /c DiR",
-	}
+var cmdLineTests = []string{
+	"",
+	"test",
+	"C^OMMAND /C DIR",
+	"\"command\" /c DiR",
+}
 
-	for _, tc := range tests {
+func BenchmarkCMDLine(b *testing.B) {
+	for _, tc := range cmdLineTests {
 		tt := tc
 		b.Run(tt, func(b *testing.B) {
 			for i := 0; i < b.N; i++ {
@@ -24,3 +27,20 @@ func BenchmarkCMDLine(b *testing.B) {
 		})
 	}
 }
+
+func FuzzCMDLine(f *testing.F) {
+	for _, tc := range cmdLineTests {
+		f.Add(tc)
+	}
+	f.Fuzz(func(t *testing.T, tc string) {
+		data, err := cmdLine(tc)
+		if err != nil {
+			t.Error(err)
+		}
+
+		// Check simple expectations
+		if strings.ContainsAny(data, `\"'^,;'ABCDEFGHIJKLMNOPQRSTUVWXYZ`) {
+			t.Errorf("unexpected characters in output %s for input %s", data, tc)
+		}
+	})
+}
diff --git a/transformations/escape_seq_decode_test.go b/transformations/escape_seq_decode_test.go
new file mode 100644
index 0000000..da51306
--- /dev/null
+++ b/transformations/escape_seq_decode_test.go
@@ -0,0 +1,25 @@
+// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package transformations
+
+import "testing"
+
+func BenchmarkEscapeSeqDecode(b *testing.B) {
+	tests := []string{
+		"",
+		"hello world",
+		"\\a\\b\\f\\n\\r\\t\\v\\u0000\\?\\'\\\"\\0\\12\\123\\x00\\xff",
+	}
+
+	for _, tc := range tests {
+		tt := tc
+		b.Run(tt, func(b *testing.B) {
+			for i := 0; i < b.N; i++ {
+				if _, err := escapeSeqDecode(tt); err != nil {
+					b.Fatal(err)
+				}
+			}
+		})
+	}
+}
diff --git a/transformations/js_decode_test.go b/transformations/js_decode_test.go
new file mode 100644
index 0000000..faebf36
--- /dev/null
+++ b/transformations/js_decode_test.go
@@ -0,0 +1,25 @@
+// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package transformations
+
+import "testing"
+
+func BenchmarkJSDecode(b *testing.B) {
+	tests := []string{
+		"",
+		"hello world",
+		"\\a\\b\\f\\n\\r\\t\\v\\u0000\\?\\'\\\"\\0\\12\\123\\x00\\xff",
+	}
+
+	for _, tc := range tests {
+		tt := tc
+		b.Run(tt, func(b *testing.B) {
+			for i := 0; i < b.N; i++ {
+				if _, err := jsDecode(tt); err != nil {
+					b.Fatal(err)
+				}
+			}
+		})
+	}
+}