Repository: corazawaf/coraza
Pull request #539 (2022-12-22 11:39:27)

Write Body buffer: adds Limit check, overflow check and limits comments
This PR proposes to:
- ~~Check `Limit` (and not just `Memory Limit`) while writing to body buffers. It leads to do not write to a temporary file bodies that exceeded the data that is going to be analyzed~~ **Update**: Reverted. See comment below
- Add an int overflow check when the current length of the buffer and the new data size is added (Follows https://github.com/corazawaf/coraza/pull/503#issuecomment-1348328382).
- Add comments about machine dependenent memory limits. Internally, `bytes.buffer` relies on [`maxInt` for growth logic](https://github.com/golang/go/blob/go1.19.4/src/bytes/buffer.go#L138). It is the first limit that we face, instead of Bodybuffer length.

Precedes https://github.com/corazawaf/coraza/pull/503


Patch:
diff --git a/RATIONALE.md b/RATIONALE.md
new file mode 100644
index 0000000..74c2635
--- /dev/null
+++ b/RATIONALE.md
@@ -0,0 +1,15 @@
+# Rationale
+
+This document contains some rationale around certain decisions or explain potential failure modes in Coraza.
+
+## Why limits should be chosen very carefully
+
+Coraza can inspect a request body and resolve its legitimacy before it reaches upstream by buffering the body payload, analyzing it, and then sending it upstream if no threads are detected. The main issue with buffering is that it can become a potential attack door by itself if not handled correctly. The buffering process occurs as follows:
+
+1. First, Coraza attempts to buffer the body in memory up to a limit defined by the directive `SecRequestBodyMemoryLimit`, by default Coraza sets `131072`.
+2. If the body payload is bigger than the memory limit, Coraza moves off the memory buffering and buffers the body in the disk, up to the limit defined by the directive `SecRequestBodyLimit`. By default Coraza sets this limit to `13107200`.
+
+Both actions represent a risk when the configuration isn't defensive enough:
+
+1. Buffering in memory, when the limit is high can cause OOM in the system as there is no soft or hard limit on how much memory can be spent on buffering, the OOM will not only affect the attack request but the entire process. It is recommended to keep the default value or lower.
+2. Buffering in the disk, when not setting the right action can become problematic: an attacker could send huge payloads which will be stored in the disk and without proper mitigation (e.g. hard limit on request size or rate limiting) it could fill up the disk, causing degraded functioning in the host. One way to prevent this is to use `SecRequestBodyLimitAction reject` setting, meaning that beyond the request body limit, the request is rejected and no more bytes are written to the disk. Once the request is finished, the buffered file is deleted.
diff --git a/actions/prepend.go b/actions/prepend.go
index bc8571f..48fa606 100644
--- a/actions/prepend.go
+++ b/actions/prepend.go
@@ -36,6 +36,7 @@ func (a *prependFn) Evaluate(r rules.RuleMetadata, txS rules.TransactionState) {
 	buf := corazawaf.NewBodyBuffer(types.BodyBufferOptions{
 		TmpPath:     tx.WAF.TmpDir,
 		MemoryLimit: tx.WAF.RequestBodyInMemoryLimit,
+		Limit:       tx.WAF.RequestBodyLimit,
 	})
 
 	_, err := buf.Write([]byte(data))
diff --git a/config.go b/config.go
index ceda8fb..628a9d1 100644
--- a/config.go
+++ b/config.go
@@ -55,8 +55,8 @@ func NewWAFConfig() WAFConfig {
 
 // RequestBodyConfig controls access to the request body.
 type RequestBodyConfig interface {
-	// WithLimit sets the maximum number of bytes that can be read from the request body. Bytes beyond that set
-	// in WithInMemoryLimit will be buffered to disk.
+	// WithLimit sets the maximum number of bytes that can be read from the request body.
+	// A request body going beyond WithInMemoryLimit will result in being buffered to disk.
 	WithLimit(limit int) RequestBodyConfig
 
 	// WithInMemoryLimit sets the maximum number of bytes that can be read from the request body and buffered in memory.
@@ -71,8 +71,13 @@ func NewRequestBodyConfig() RequestBodyConfig {
 // ResponseBodyConfig controls access to the response body.
 type ResponseBodyConfig interface {
 	// WithLimit sets the maximum number of bytes that can be read from the response body and buffered in memory.
+	// Keep in mind your machine architecture and memory/swap limits while setting it.
+	// E.g. 32-bit machine tested limit: 1073741824 (1GiB)
 	WithLimit(limit int) ResponseBodyConfig
 
+	// WithInMemoryLimit is not implemented for ResponseBody. The body will be just buffered in memory,
+	// therefore WithLimit already sets the in memory threshold
+
 	// WithMimeTypes sets the mime types of responses that will be processed.
 	WithMimeTypes(mimeTypes []string) ResponseBodyConfig
 }
diff --git a/http/interceptor.go b/http/interceptor.go
index 936e686..3ed2cc0 100644
--- a/http/interceptor.go
+++ b/http/interceptor.go
@@ -57,6 +57,7 @@ func (i *rwInterceptor) Write(b []byte) (int, error) {
 	if i.tx.IsResponseBodyAccessible() {
 		// we only buffer the response body if we are going to access
 		// to it, otherwise we just send it to the response writer.
+		// TODO: rely on tx.ReadResponseBodyFrom
 		return i.tx.ResponseBodyWriter().Write(b)
 	}
 
@@ -87,16 +88,16 @@ func wrap(w http.ResponseWriter, r *http.Request, tx types.Transaction) (
 		// as body hasn't being analized yet.
 		if tx.IsInterrupted() {
 			// phase 4 interruption stops execution
-			w.WriteHeader(i.statusCode)
+			i.w.WriteHeader(i.statusCode)
 			return nil
 		}
 
 		if tx.IsResponseBodyAccessible() && tx.IsResponseBodyProcessable() {
 			if it, err := tx.ProcessResponseBody(); err != nil {
-				w.WriteHeader(http.StatusInternalServerError)
+				i.w.WriteHeader(http.StatusInternalServerError)
 				return err
 			} else if it != nil {
-				w.WriteHeader(obtainStatusCodeFromInterruptionOrDefault(it, i.statusCode))
+				i.w.WriteHeader(obtainStatusCodeFromInterruptionOrDefault(it, i.statusCode))
 				return nil
 			}
 
diff --git a/http/middleware.go b/http/middleware.go
index 0a401c0..16345ae 100644
--- a/http/middleware.go
+++ b/http/middleware.go
@@ -73,6 +73,8 @@ func processRequest(tx types.Transaction, req *http.Request) (*types.Interruptio
 				return nil, fmt.Errorf("failed to get the request body: %s", err.Error())
 			}
 
+			// Adds all remaining bytes beyond the coraza limit to its buffer
+			// It happens when the partial body has been processed and it did not trigger an interruption
 			body := io.MultiReader(rbr, req.Body)
 			// req.Body is transparently reinizialied with a new io.ReadCloser.
 			// The http handler will be able to read it.
diff --git a/internal/corazawaf/body_buffer.go b/internal/corazawaf/body_buffer.go
index 4068a71..53781b7 100644
--- a/internal/corazawaf/body_buffer.go
+++ b/internal/corazawaf/body_buffer.go
@@ -5,6 +5,7 @@ package corazawaf
 
 import (
 	"bytes"
+	"errors"
 	"io"
 	"os"
 
@@ -50,15 +51,25 @@ func (br *BodyBuffer) Write(data []byte) (n int, err error) {
 		return 0, nil
 	}
 
-	l := int64(len(data)) + br.length
-	if l > br.options.MemoryLimit {
+	if br.length == br.options.Limit || br.length >= (br.options.Limit-int64(len(data))) {
+		// Write has been called without checking the Limit, it should never happend. Raising an error.
+		// The buffers are private and populated only by WriteRequestBody, ReadRequestBodyFrom, and similar functions
+		// that have to perform limit checks before calling Write()
+		return 0, errors.New("Limit reached while writing")
+	}
+	targetLen := br.length + int64(len(data))
+
+	// Check if memory limits are reached
+	// Even if Overflow is explicitly checked, MemoryLimit real limits are below maxInt and machine dependenent.
+	// bytes.Buffer growth is platform dependent with a growth rate capped at 2x. If the buffer can't grow it will panic with ErrTooLarge.
+	// See https://github.com/golang/go/blob/go1.19.4/src/bytes/buffer.go#L117 and https://go-review.googlesource.com/c/go/+/349994
+	// Local tests show these buffer limits:
+	// 32-bit machine: 2147483647 (2^30, 1GiB)
+	// 64-bit machine: 34359738368 (2^35, 32GiB) (Not reached the ErrTooLarge panic, the OS triggered an OOM)
+	if targetLen > br.options.MemoryLimit {
 		if environment.IsTinyGo {
-			maxWritingDataLen := br.options.MemoryLimit - br.length
-			if maxWritingDataLen == 0 {
-				return 0, nil
-			}
-			br.length = br.options.MemoryLimit
-			return br.buffer.Write(data[:maxWritingDataLen])
+			// TinyGo MemoryLimit should be equal to Limit. Therefore, Write function has been called without Limit check.
+			return 0, errors.New("MemoryLimit reached while writing")
 		} else {
 			if br.writer == nil {
 				br.writer, err = os.CreateTemp(br.options.TmpPath, "body*")
@@ -71,12 +82,12 @@ func (br *BodyBuffer) Write(data []byte) (n int, err error) {
 				}
 				br.buffer.Reset()
 			}
-			br.length = l
+			br.length = targetLen
 			return br.writer.Write(data)
 		}
 	}
 
-	br.length = l
+	br.length = targetLen
 	return br.buffer.Write(data)
 }
 
diff --git a/internal/corazawaf/transaction.go b/internal/corazawaf/transaction.go
index e122e2c..5c9ce8a 100644
--- a/internal/corazawaf/transaction.go
+++ b/internal/corazawaf/transaction.go
@@ -8,6 +8,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"math"
 	"mime"
 	"net/url"
 	"path/filepath"
@@ -773,6 +774,7 @@ func (tx *Transaction) ProcessRequestHeaders() *types.Interruption {
 
 func setAndReturnBodyLimitInterruption(tx *Transaction) (*types.Interruption, int, error) {
 	tx.variables.inboundErrorData.Set("1")
+	tx.DebugLogger().Warn("Disrupting transaction with body size above the configured limit (Action Reject)")
 	tx.interruption = &types.Interruption{
 		Status: 403,
 		Action: "deny",
@@ -781,8 +783,8 @@ func setAndReturnBodyLimitInterruption(tx *Transaction) (*types.Interruption, in
 }
 
 // WriteRequestBody writes bytes from a slice of bytes into the request body,
-// it returns an interuption if the writing bytes go beyond the request body limit.
-// It won't copy the bytes if the body access isn't accesible.
+// it returns an interruption if the writing bytes go beyond the request body limit.
+// It won't copy the bytes if the body access isn't accessible.
 func (tx *Transaction) WriteRequestBody(b []byte) (*types.Interruption, int, error) {
 	if tx.RuleEngine == types.RuleEngineOff {
 		return nil, 0, nil
@@ -794,12 +796,12 @@ func (tx *Transaction) WriteRequestBody(b []byte) (*types.Interruption, int, err
 
 	if tx.RequestBodyLimit == tx.requestBodyBuffer.length {
 		// tx.RequestBodyLimit will never be zero so if this happened, we have an
-		// interruption for sure.
-		if tx.WAF.RequestBodyLimitAction == types.RequestBodyLimitActionReject {
+		// interruption (that has been previously raised, but ignored by the connector) for sure.
+		if tx.WAF.RequestBodyLimitAction == types.BodyLimitActionReject {
 			return tx.interruption, 0, nil
 		}
 
-		if tx.WAF.RequestBodyLimitAction == types.RequestBodyLimitActionProcessPartial {
+		if tx.WAF.RequestBodyLimitAction == types.BodyLimitActionProcessPartial {
 			return nil, 0, nil
 		}
 	}
@@ -808,14 +810,20 @@ func (tx *Transaction) WriteRequestBody(b []byte) (*types.Interruption, int, err
 		writingBytes          = int64(len(b))
 		runProcessRequestBody = false
 	)
+	// Overflow check
+	if tx.requestBodyBuffer.length >= (math.MaxInt64 - writingBytes) {
+		// Overflow, failing. MaxInt64 is not a realistic payload size. Furthermore, it has been tested that
+		// bytes.Buffer does not work with this kind of sizes. See comments in BodyBuffer Write(data []byte)
+		return nil, 0, errors.New("Overflow reached while writing request body")
+	}
 
 	if tx.requestBodyBuffer.length+writingBytes >= tx.RequestBodyLimit {
-		if tx.WAF.RequestBodyLimitAction == types.RequestBodyLimitActionReject {
+		if tx.WAF.RequestBodyLimitAction == types.BodyLimitActionReject {
 			// We interrupt this transaction in case RequestBodyLimitAction is Reject
 			return setAndReturnBodyLimitInterruption(tx)
 		}
 
-		if tx.WAF.RequestBodyLimitAction == types.RequestBodyLimitActionProcessPartial {
+		if tx.WAF.RequestBodyLimitAction == types.BodyLimitActionProcessPartial {
 			writingBytes = tx.RequestBodyLimit - tx.requestBodyBuffer.length
 			runProcessRequestBody = true
 		}
@@ -827,6 +835,7 @@ func (tx *Transaction) WriteRequestBody(b []byte) (*types.Interruption, int, err
 	}
 
 	if runProcessRequestBody {
+		tx.DebugLogger().Warn("Processing request body whose size reached the configured limit (Action ProcessPartial)")
 		_, err = tx.ProcessRequestBody()
 	}
 	return tx.interruption, int(w), err
@@ -838,8 +847,8 @@ type ByteLenger interface {
 }
 
 // ReadRequestBodyFrom writes bytes from a reader into the request body
-// it returns an interuption if the writing bytes go beyond the request body limit.
-// It won't read the reader if the body access isn't accesible.
+// it returns an interruption if the writing bytes go beyond the request body limit.
+// It won't read the reader if the body access isn't accessible.
 func (tx *Transaction) ReadRequestBodyFrom(r io.Reader) (*types.Interruption, int, error) {
 	if tx.RuleEngine == types.RuleEngineOff {
 		return nil, 0, nil
@@ -850,11 +859,13 @@ func (tx *Transaction) ReadRequestBodyFrom(r io.Reader) (*types.Interruption, in
 	}
 
 	if tx.RequestBodyLimit == tx.requestBodyBuffer.length {
-		if tx.WAF.RequestBodyLimitAction == types.RequestBodyLimitActionReject {
+		// tx.RequestBodyLimit will never be zero so if this happened, we have an
+		// interruption (that has been previously raised, but ignored by the connector) for sure.
+		if tx.WAF.RequestBodyLimitAction == types.BodyLimitActionReject {
 			return tx.interruption, 0, nil
 		}
 
-		if tx.WAF.RequestBodyLimitAction == types.RequestBodyLimitActionProcessPartial {
+		if tx.WAF.RequestBodyLimitAction == types.BodyLimitActionProcessPartial {
 			return nil, 0, nil
 		}
 	}
@@ -865,12 +876,18 @@ func (tx *Transaction) ReadRequestBodyFrom(r io.Reader) (*types.Interruption, in
 	)
 	if l, ok := r.(ByteLenger); ok {
 		writingBytes = int64(l.Len())
+		// Overflow check
+		if tx.requestBodyBuffer.length >= (math.MaxInt64 - writingBytes) {
+			// Overflow, failing. MaxInt64 is not a realistic payload size. Furthermore, it has been tested that
+			// bytes.Buffer does not work with this kind of sizes. See comments in BodyBuffer Write(data []byte)
+			return nil, 0, errors.New("Overflow reached while writing request body")
+		}
 		if tx.requestBodyBuffer.length+writingBytes >= tx.RequestBodyLimit {
-			if tx.WAF.RequestBodyLimitAction == types.RequestBodyLimitActionReject {
+			if tx.WAF.RequestBodyLimitAction == types.BodyLimitActionReject {
 				return setAndReturnBodyLimitInterruption(tx)
 			}
 
-			if tx.WAF.RequestBodyLimitAction == types.RequestBodyLimitActionProcessPartial {
+			if tx.WAF.RequestBodyLimitAction == types.BodyLimitActionProcessPartial {
 				writingBytes = tx.RequestBodyLimit - tx.requestBodyBuffer.length
 				runProcessRequestBody = true
 			}
@@ -885,17 +902,18 @@ func (tx *Transaction) ReadRequestBodyFrom(r io.Reader) (*types.Interruption, in
 	}
 
 	if tx.requestBodyBuffer.length == tx.RequestBodyLimit {
-		if tx.WAF.RequestBodyLimitAction == types.RequestBodyLimitActionReject {
+		if tx.WAF.RequestBodyLimitAction == types.BodyLimitActionReject {
 			return setAndReturnBodyLimitInterruption(tx)
 		}
 
-		if tx.WAF.RequestBodyLimitAction == types.RequestBodyLimitActionProcessPartial {
+		if tx.WAF.RequestBodyLimitAction == types.BodyLimitActionProcessPartial {
 			runProcessRequestBody = true
 		}
 	}
 
 	err = nil
 	if runProcessRequestBody {
+		tx.DebugLogger().Warn("Processing request body whose size reached the configured limit (Action ProcessPartial)")
 		_, err = tx.ProcessRequestBody()
 	}
 	return tx.interruption, int(w), err
@@ -915,7 +933,7 @@ func (tx *Transaction) ProcessRequestBody() (*types.Interruption, error) {
 
 	if tx.LastPhase >= types.PhaseRequestBody {
 		// Phase already evaluated
-		tx.WAF.Logger.Error("ProcessRequestBody has already been called")
+		tx.WAF.Logger.Warn("ProcessRequestBody has already been called")
 		return tx.interruption, nil
 	}
 
@@ -1019,7 +1037,7 @@ func (tx *Transaction) ResponseBodyWriter() io.Writer {
 	return tx.ResponseBodyBuffer
 }
 
-// ProcessResponseBody Perform the request body (if any)
+// ProcessResponseBody Perform the response body (if any)
 //
 // This method perform the analysis on the request body. It is optional to
 // call that method. If this API consumer already know that there isn't a
@@ -1033,7 +1051,7 @@ func (tx *Transaction) ProcessResponseBody() (*types.Interruption, error) {
 
 	if tx.LastPhase >= types.PhaseResponseBody {
 		// Phase already evaluated
-		tx.WAF.Logger.Error("ProcessResponseBody has already been called")
+		tx.WAF.Logger.Warn("ProcessResponseBody has already been called")
 		return tx.interruption, nil
 	}
 
diff --git a/internal/corazawaf/waf.go b/internal/corazawaf/waf.go
index ad16893..9ce71a1 100644
--- a/internal/corazawaf/waf.go
+++ b/internal/corazawaf/waf.go
@@ -63,12 +63,6 @@ type WAF struct {
 	// Defines if rules are going to be evaluated
 	RuleEngine types.RuleEngineStatus
 
-	// If true, transaction will fail if response size is bigger than the page limit
-	RejectOnResponseBodyLimit bool
-
-	// If true, transaction will fail if request size is bigger than the page limit
-	RejectOnRequestBodyLimit bool
-
 	// Responses will only be loaded if mime is listed here
 	ResponseBodyMimeTypes []string
 
@@ -108,7 +102,9 @@ type WAF struct {
 
 	RequestBodyNoFilesLimit int64
 
-	RequestBodyLimitAction types.RequestBodyLimitAction
+	RequestBodyLimitAction types.BodyLimitAction
+
+	ResponseBodyLimitAction types.BodyLimitAction
 
 	ArgumentSeparator string
 
@@ -178,10 +174,13 @@ func (w *WAF) newTransactionWithID(id string) *Transaction {
 		tx.requestBodyBuffer = NewBodyBuffer(types.BodyBufferOptions{
 			TmpPath:     w.TmpDir,
 			MemoryLimit: w.RequestBodyInMemoryLimit,
+			Limit:       w.ResponseBodyLimit,
 		})
 		tx.ResponseBodyBuffer = NewBodyBuffer(types.BodyBufferOptions{
-			TmpPath:     w.TmpDir,
-			MemoryLimit: w.RequestBodyInMemoryLimit,
+			TmpPath: w.TmpDir,
+			// the response body is just buffered in memory. Therefore, Limit and MemoryLimit are equal.
+			MemoryLimit: w.ResponseBodyLimit,
+			Limit:       w.ResponseBodyLimit,
 		})
 		tx.variables = *NewTransactionVariables()
 		tx.transformationCache = map[transformationKey]*transformationValue{}
@@ -270,16 +269,18 @@ func NewWAF() *WAF {
 		AuditLogWriter:           logWriter,
 		AuditEngine:              types.AuditEngineOff,
 		AuditLogParts:            types.AuditLogParts("ABCFHZ"),
+		RequestBodyAccess:        false,
 		RequestBodyInMemoryLimit: 131072,
 		RequestBodyLimit:         134217728, // 10mb
+		RequestBodyLimitAction:   types.BodyLimitActionReject,
 		ResponseBodyMimeTypes:    []string{"text/html", "text/plain"},
 		ResponseBodyLimit:        524288,
+		ResponseBodyLimitAction:  types.BodyLimitActionReject,
 		ResponseBodyAccess:       false,
 		RuleEngine:               types.RuleEngineOn,
 		Rules:                    NewRuleGroup(),
 		TmpDir:                   "/tmp",
 		AuditLogRelevantStatus:   regexp.MustCompile(`.*`),
-		RequestBodyAccess:        false,
 		Logger:                   logger,
 	}
 	// We initialize a basic audit log writer that discards output
diff --git a/internal/seclang/directives.go b/internal/seclang/directives.go
index ba9129f..6daa8fa 100644
--- a/internal/seclang/directives.go
+++ b/internal/seclang/directives.go
@@ -170,7 +170,14 @@ func directiveSecResponseBodyMimeType(options *DirectiveOptions) error {
 }
 
 func directiveSecResponseBodyLimitAction(options *DirectiveOptions) error {
-	options.WAF.RejectOnResponseBodyLimit = strings.ToLower(options.Opts) == "reject"
+	switch strings.ToLower(options.Opts) {
+	case "reject":
+		options.WAF.ResponseBodyLimitAction = types.BodyLimitActionReject
+	case "processpartial":
+		options.WAF.ResponseBodyLimitAction = types.BodyLimitActionProcessPartial
+	default:
+		return errors.New("syntax error: SecResponseBodyLimitAction [Reject/ProcessPartial]")
+	}
 	return nil
 }
 
@@ -181,7 +188,14 @@ func directiveSecResponseBodyLimit(options *DirectiveOptions) error {
 }
 
 func directiveSecRequestBodyLimitAction(options *DirectiveOptions) error {
-	options.WAF.RejectOnRequestBodyLimit = strings.ToLower(options.Opts) == "reject"
+	switch strings.ToLower(options.Opts) {
+	case "reject":
+		options.WAF.RequestBodyLimitAction = types.BodyLimitActionReject
+	case "processpartial":
+		options.WAF.RequestBodyLimitAction = types.BodyLimitActionProcessPartial
+	default:
+		return errors.New("syntax error: SecRequestBodyLimitAction [Reject/ProcessPartial]")
+	}
 	return nil
 }
 
diff --git a/types/waf.go b/types/waf.go
index 724f9ca..c18860b 100644
--- a/types/waf.go
+++ b/types/waf.go
@@ -97,31 +97,20 @@ func (re RuleEngineStatus) String() string {
 	return "unknown"
 }
 
-// RequestBodyLimitAction represents the action
+// BodyLimitAction represents the action
 // to take when the request body size exceeds
 // the configured limit.
-type RequestBodyLimitAction int
+type BodyLimitAction int
 
 const (
-	// RequestBodyLimitActionProcessPartial will process the request body
+	// BodyLimitActionProcessPartial will process the request body
 	// up to the limit and then reject the request
-	RequestBodyLimitActionProcessPartial RequestBodyLimitAction = 0
-	// RequestBodyLimitActionReject will reject the request in case
+	BodyLimitActionProcessPartial BodyLimitAction = 0
+	// BodyLimitActionReject will reject the request in case
 	// the request body size exceeds the configured limit
-	RequestBodyLimitActionReject RequestBodyLimitAction = 1
+	BodyLimitActionReject BodyLimitAction = 1
 )
 
-// ParseRequestBodyLimitAction parses the request body limit action
-func ParseRequestBodyLimitAction(rbla string) (RequestBodyLimitAction, error) {
-	switch strings.ToLower(rbla) {
-	case "processpartial":
-		return RequestBodyLimitActionProcessPartial, nil
-	case "reject":
-		return RequestBodyLimitActionReject, nil
-	}
-	return -1, fmt.Errorf("invalid request body limit action: %s", rbla)
-}
-
 type auditLogPart byte
 
 // AuditLogParts represents the parts of the audit log
@@ -193,4 +182,6 @@ type BodyBufferOptions struct {
 	// MemoryLimit is the maximum amount of memory to be stored in memory
 	// Once the limit is reached, the file will be stored on disk
 	MemoryLimit int64
+	// Limit is the overall maximum amount of memory to be buffered
+	Limit int64
 }


Test patch:
diff --git a/examples/http-server/main_test.go b/examples/http-server/main_test.go
index 10a149f..78d9d65 100644
--- a/examples/http-server/main_test.go
+++ b/examples/http-server/main_test.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"bytes"
 	"log"
 	"net/http"
 	"net/http/httptest"
@@ -26,15 +27,26 @@ func doGetRequest(t *testing.T, getPath string) int {
 	return resp.StatusCode
 }
 
+func doPostRequest(t *testing.T, postPath string, data []byte) int {
+	t.Helper()
+	resp, err := http.Post(postPath, "application/x-www-form-urlencoded", bytes.NewBuffer(data))
+	if err != nil {
+		log.Fatalln(err)
+	}
+	resp.Body.Close()
+	return resp.StatusCode
+}
+
 func TestHttpServer(t *testing.T) {
 	tests := []struct {
 		name      string
 		path      string
 		expStatus int
 		envVars   map[string]string
+		body      []byte // if body is populated, POST request is sent
 	}{
-		{"negative", "/", 200, nil},
-		{"positive for query parameter", "/?id=0", 403, nil},
+		{"negative", "/", 200, nil, nil},
+		{"positive for query parameter", "/?id=0", 403, nil, nil},
 		{
 			"positive for response body",
 			"/",
@@ -43,6 +55,7 @@ func TestHttpServer(t *testing.T) {
 				"DIRECTIVES_FILE": "./testdata/response-body.conf",
 				"RESPONSE_BODY":   "creditcard",
 			},
+			nil,
 		},
 		{
 			"positive for response header",
@@ -52,11 +65,33 @@ func TestHttpServer(t *testing.T) {
 				"DIRECTIVES_FILE":  "./testdata/response-headers.conf",
 				"RESPONSE_HEADERS": "foo:bar",
 			},
+			nil,
 		},
+		{
+			"negative for request body process partial (payload beyond processed body)",
+			"/",
+			200,
+			map[string]string{
+				"DIRECTIVES_FILE": "./testdata/request-body-limits-processpartial.conf",
+			},
+			[]byte("beyond the limit script"),
+		},
+		// TODO(M4tteoP) uncomment after merging WriteRsponseBody logic
+		// {
+		// 	"positive for response body limit reject",
+		// 	"/",
+		// 	403,
+		// 	map[string]string{
+		// 		"DIRECTIVES_FILE": "./testdata/response-body-limits-reject.conf",
+		// 		"RESPONSE_BODY":   "response body beyond the limit",
+		// 	},
+		// 	nil,
+		// },
 	}
 	// Perform tests
 	for _, tc := range tests {
 		tt := tc
+		var statusCode int
 		t.Run(tt.name, func(t *testing.T) {
 			if len(tt.envVars) > 0 {
 				for k, v := range tt.envVars {
@@ -68,8 +103,11 @@ func TestHttpServer(t *testing.T) {
 			// Spin up the test server
 			testServer := setupTestServer(t)
 			defer testServer.Close()
-
-			statusCode := doGetRequest(t, testServer.URL+tt.path)
+			if tt.body == nil {
+				statusCode = doGetRequest(t, testServer.URL+tt.path)
+			} else {
+				statusCode = doPostRequest(t, testServer.URL+tt.path, tt.body)
+			}
 			if want, have := tt.expStatus, statusCode; want != have {
 				t.Errorf("Unexpected status code, want: %d, have: %d", want, have)
 			}
diff --git a/examples/http-server/testdata/request-body-limits-processpartial.conf b/examples/http-server/testdata/request-body-limits-processpartial.conf
new file mode 100644
index 0000000..8d1c897
--- /dev/null
+++ b/examples/http-server/testdata/request-body-limits-processpartial.conf
@@ -0,0 +1,7 @@
+SecDebugLogLevel 5
+SecDebugLog /dev/stdout
+SecRequestBodyAccess On
+SecRequestBodyInMemoryLimit 5
+SecRequestBodyLimit 6
+SecRequestBodyLimitAction ProcessPartial
+SecRule REQUEST_BODY "@contains script" "id:200, phase:2, deny, status:403, msg:'Invalid request body',log,auditlog"
diff --git a/examples/http-server/testdata/response-body-limits-reject.conf b/examples/http-server/testdata/response-body-limits-reject.conf
new file mode 100644
index 0000000..6af2f57
--- /dev/null
+++ b/examples/http-server/testdata/response-body-limits-reject.conf
@@ -0,0 +1,6 @@
+SecDebugLogLevel 5
+SecDebugLog /dev/stdout
+SecResponseBodyAccess On
+SecResponseBodyMimeType text/plain
+SecResponseBodyLimit 6
+SecResponseBodyLimitAction Reject
diff --git a/http/middleware_test.go b/http/middleware_test.go
index e8db056..6cec612 100644
--- a/http/middleware_test.go
+++ b/http/middleware_test.go
@@ -294,6 +294,7 @@ func TestHttpServer(t *testing.T) {
 	SecRequestBodyAccess On
 	SecResponseBodyAccess On
 	SecResponseBodyMimeType text/plain
+	SecRequestBodyLimitAction ProcessPartial
 	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"
diff --git a/internal/corazawaf/body_buffer_test.go b/internal/corazawaf/body_buffer_test.go
index 0226948..6016ecb 100644
--- a/internal/corazawaf/body_buffer_test.go
+++ b/internal/corazawaf/body_buffer_test.go
@@ -17,6 +17,7 @@ func TestBodyReaderMemory(t *testing.T) {
 	br := NewBodyBuffer(types.BodyBufferOptions{
 		TmpPath:     t.TempDir(),
 		MemoryLimit: 500,
+		Limit:       500,
 	})
 	if _, err := br.Write([]byte("test")); err != nil {
 		t.Error(err)
@@ -44,6 +45,7 @@ func TestBodyReaderFile(t *testing.T) {
 	br := NewBodyBuffer(types.BodyBufferOptions{
 		TmpPath:     t.TempDir(),
 		MemoryLimit: 1,
+		Limit:       100,
 	})
 	if _, err := br.Write([]byte("test")); err != nil {
 		t.Error(err)
@@ -74,6 +76,7 @@ func TestBodyReaderWriteFromReader(t *testing.T) {
 	br := NewBodyBuffer(types.BodyBufferOptions{
 		TmpPath:     t.TempDir(),
 		MemoryLimit: 5,
+		Limit:       5,
 	})
 	b := strings.NewReader("test")
 	if _, err := io.Copy(br, b); err != nil {
@@ -92,3 +95,15 @@ func TestBodyReaderWriteFromReader(t *testing.T) {
 	}
 	_ = br.Reset()
 }
+
+func TestWriteOverLimit(t *testing.T) {
+	br := NewBodyBuffer(types.BodyBufferOptions{
+		MemoryLimit: 0,
+		Limit:       2,
+	})
+	_, err := br.Write([]byte{'a', 'b', 'c'})
+	if err == nil {
+		t.Error("unexpected successful Write above Limit")
+	}
+	_ = br.Reset()
+}
diff --git a/internal/corazawaf/body_buffer_tinygo_test.go b/internal/corazawaf/body_buffer_tinygo_test.go
deleted file mode 100644
index 3433731..0000000
--- a/internal/corazawaf/body_buffer_tinygo_test.go
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
-// SPDX-License-Identifier: Apache-2.0
-
-//go:build tinygo
-// +build tinygo
-
-package corazawaf
-
-import (
-	"testing"
-
-	"github.com/corazawaf/coraza/v3/types"
-)
-
-func TestWriteOverLimit(t *testing.T) {
-	br := NewBodyBuffer(types.BodyBufferOptions{
-		MemoryLimit: 2,
-	})
-	defer br.Reset()
-	n, err := br.Write([]byte{'a', 'b', 'c'})
-	if err != nil {
-		t.Fatalf("unexpected error: %s", err.Error())
-	}
-
-	if want, have := 2, n; want != have {
-		t.Errorf("unexpected number of bytes in write, want: %d, have: %d", want, have)
-	}
-
-	if want, have := "ab", br.buffer.String(); want != have {
-		t.Errorf("unexpected writen bytes, want: %q, have: %q", want, have)
-	}
-}
diff --git a/internal/corazawaf/transaction_test.go b/internal/corazawaf/transaction_test.go
index a085918..ce3c929 100644
--- a/internal/corazawaf/transaction_test.go
+++ b/internal/corazawaf/transaction_test.go
@@ -161,24 +161,24 @@ func TestWriteRequestBody(t *testing.T) {
 	testCases := []struct {
 		name                   string
 		requestBodyLimit       int
-		requestBodyLimitAction types.RequestBodyLimitAction
+		requestBodyLimitAction types.BodyLimitAction
 		shouldInterrupt        bool
 	}{
 		{
 			name:                   "LimitNotReached",
 			requestBodyLimit:       urlencodedBodyLen + 2,
-			requestBodyLimitAction: types.RequestBodyLimitAction(-1),
+			requestBodyLimitAction: types.BodyLimitAction(-1),
 		},
 		{
 			name:                   "LimitReachedAndRejects",
 			requestBodyLimit:       urlencodedBodyLen - 3,
-			requestBodyLimitAction: types.RequestBodyLimitActionReject,
+			requestBodyLimitAction: types.BodyLimitActionReject,
 			shouldInterrupt:        true,
 		},
 		{
 			name:                   "LimitReachedAndPartialProcessing",
 			requestBodyLimit:       urlencodedBodyLen - 3,
-			requestBodyLimitAction: types.RequestBodyLimitActionProcessPartial,
+			requestBodyLimitAction: types.BodyLimitActionProcessPartial,
 		},
 	}
 
@@ -250,17 +250,17 @@ func TestWriteRequestBody(t *testing.T) {
 
 func TestWriteRequestBodyOnLimitReached(t *testing.T) {
 	testCases := map[string]struct {
-		requestBodyLimitAction  types.RequestBodyLimitAction
+		requestBodyLimitAction  types.BodyLimitAction
 		preexistingInterruption *types.Interruption
 	}{
 		"reject": {
-			requestBodyLimitAction: types.RequestBodyLimitActionReject,
+			requestBodyLimitAction: types.BodyLimitActionReject,
 			preexistingInterruption: &types.Interruption{
 				RuleID: 123,
 			},
 		},
 		"partial processing": {
-			requestBodyLimitAction: types.RequestBodyLimitActionProcessPartial,
+			requestBodyLimitAction: types.BodyLimitActionProcessPartial,
 		},
 	}
 
@@ -666,25 +666,31 @@ func TestTransactionSyncPool(t *testing.T) {
 	}
 }
 
-func TestTxPhase4Magic(t *testing.T) {
-	waf := NewWAF()
-	tx := waf.NewTransaction()
-	tx.AddResponseHeader("content-type", "text/html")
-	tx.ResponseBodyAccess = true
-	tx.WAF.ResponseBodyLimit = 3
-	if _, err := tx.ResponseBodyBuffer.Write([]byte("more bytes")); err != nil {
-		t.Error(err)
-	}
-	if _, err := tx.ProcessResponseBody(); err != nil {
-		t.Error(err)
-	}
-	if tx.variables.outboundDataError.String() != "1" {
-		t.Error("failed to set outbound data error")
-	}
-	if tx.variables.responseBody.String() != "mor" {
-		t.Error("failed to set response body")
-	}
-}
+// TODO: enable again after implemeting tx.ReadResponseBodyFrom
+// Adding Limit check inside body_buffer, we have to rely on tx.ReadResponseBodyFrom instead of
+// directly calling Write, otherwise error "Limit reached while writing" is raised and the body is not
+// partially written like this test expects.
+
+// func TestTxPhase4Magic(t *testing.T) {
+// 	waf := NewWAF()
+// 	waf.ResponseBodyAccess = true
+// 	waf.ResponseBodyLimit = 3
+// 	waf.ResponseBodyLimitAction = types.BodyLimitActionProcessPartial
+// 	tx := waf.NewTransaction()
+// 	tx.AddResponseHeader("content-type", "text/html")
+// 	if _, err := tx.ResponseBodyBuffer.Write([]byte("more bytes")); err != nil {
+// 		t.Error(err)
+// 	}
+// 	if _, err := tx.ProcessResponseBody(); err != nil {
+// 		t.Error(err)
+// 	}
+// 	if tx.variables.outboundDataError.String() != "1" {
+// 		t.Error("failed to set outbound data error")
+// 	}
+// 	if tx.variables.responseBody.String() != "mor" {
+// 		t.Error("failed to set response body")
+// 	}
+// }
 
 func TestVariablesMatch(t *testing.T) {
 	waf := NewWAF()
diff --git a/internal/seclang/directives_test.go b/internal/seclang/directives_test.go
index 125b2ae..87e619e 100644
--- a/internal/seclang/directives_test.go
+++ b/internal/seclang/directives_test.go
@@ -129,6 +129,12 @@ func Test_directive(t *testing.T) {
 	if err := p.FromString(`SecRemoteRulesFailAction Abort`); err != nil {
 		t.Error("failed to set directive: SecRemoteRulesFailAction")
 	}
+	if err := p.FromString(`SecRequestBodyLimitAction Reject`); err != nil {
+		t.Error("failed to set directive: SecRequestBodyLimitAction")
+	}
+	if err := p.FromString(`SecResponseBodyLimitAction ProcessPartial`); err != nil {
+		t.Error("failed to set directive: SecResponseBodyLimitAction")
+	}
 }
 
 func TestSecRuleUpdateTargetBy(t *testing.T) {
diff --git a/testing/engine_test.go b/testing/engine_test.go
index adf1741..7a0876b 100644
--- a/testing/engine_test.go
+++ b/testing/engine_test.go
@@ -72,7 +72,7 @@ func TestResponse(t *testing.T) {
 	waf, err := coraza.NewWAF(
 		coraza.NewWAFConfig().
 			WithResponseBodyAccess(
-				coraza.NewResponseBodyConfig().WithLimit(10),
+				coraza.NewResponseBodyConfig().WithLimit(21),
 			),
 	)
 	if err != nil {