Repository: corazawaf/coraza
Pull request #587 (2023-01-25 17:43:08)

implements ReadResponseBodyFrom and WriteResponseBody
Follows https://github.com/corazawaf/coraza/pull/560, implemented corresponding functions for the response body. 

actions related to ContentInjection require extra care. See: https://github.com/corazawaf/coraza/pull/589

codecov-commenter: # [Codecov](https://codecov.io/gh/corazawaf/coraza/pull/587?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) Report
Base: **77.51**% // Head: **76.67**% // Decreases project coverage by **`-0.84%`** :warning:
> Coverage data is based on head [(`dc69049`)](https://codecov.io/gh/corazawaf/coraza/pull/587?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) compared to base [(`093d102`)](https://codecov.io/gh/corazawaf/coraza/commit/093d10209c6e1e7b1fe6864df016b86ec5b7ed65?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf).
> Patch coverage: 26.60% of modified lines in pull request are covered.

:mega: This organization is not using Codecov’s [GitHub App Integration](https://github.com/apps/codecov). We recommend you install it so Codecov can continue to function properly for your repositories. [Learn more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf)

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


```diff
@@            Coverage Diff             @@
##           v3/dev     #587      +/-   ##
==========================================
- Coverage   77.51%   76.67%   -0.84%     
==========================================
  Files         145      145              
  Lines        7405     7491      +86     
==========================================
+ Hits         5740     5744       +4     
- Misses       1423     1499      +76     
- Partials      242      248       +6     
```

| Flag | Coverage Δ | |
|---|---|---|
| default | `72.22% <18.34%> (-0.80%)` | :arrow_down: |
| examples | `13.43% <16.66%> (-14.51%)` | :arrow_down: |
| ftw | `54.12% <12.03%> (-0.67%)` | :arrow_down: |
| ftw-multiphase | `54.22% <12.03%> (-0.67%)` | :arrow_down: |
| tinygo | `70.19% <16.00%> (-0.78%)` | :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/587?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) | Coverage Δ | |
|---|---|---|
| [testing/engine.go](https://codecov.io/gh/corazawaf/coraza/pull/587?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-dGVzdGluZy9lbmdpbmUuZ28=) | `58.73% <0.00%> (ø)` | |
| [internal/corazawaf/transaction.go](https://codecov.io/gh/corazawaf/coraza/pull/587?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvY29yYXphd2FmL3RyYW5zYWN0aW9uLmdv) | `75.19% <17.20%> (-3.93%)` | :arrow_down: |
| [actions/append.go](https://codecov.io/gh/corazawaf/coraza/pull/587?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-YWN0aW9ucy9hcHBlbmQuZ28=) | `52.38% <50.00%> (+2.38%)` | :arrow_up: |
| [http/interceptor.go](https://codecov.io/gh/corazawaf/coraza/pull/587?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aHR0cC9pbnRlcmNlcHRvci5nbw==) | `45.45% <88.88%> (-0.22%)` | :arrow_down: |
| [actions/prepend.go](https://codecov.io/gh/corazawaf/coraza/pull/587?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-YWN0aW9ucy9wcmVwZW5kLmdv) | `56.75% <100.00%> (-1.14%)` | :arrow_down: |
| [internal/corazawaf/waf.go](https://codecov.io/gh/corazawaf/coraza/pull/587?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvY29yYXphd2FmL3dhZi5nbw==) | `90.83% <100.00%> (ø)` | |
| [examples/http-server/main.go](https://codecov.io/gh/corazawaf/coraza/pull/587?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-ZXhhbXBsZXMvaHR0cC1zZXJ2ZXIvbWFpbi5nbw==) | `51.35% <0.00%> (-18.92%)` | :arrow_down: |
| [internal/corazawaf/body\_buffer.go](https://codecov.io/gh/corazawaf/coraza/pull/587?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvY29yYXphd2FmL2JvZHlfYnVmZmVyLmdv) | `68.96% <0.00%> (-2.30%)` | :arrow_down: |
| [operators/pm.go](https://codecov.io/gh/corazawaf/coraza/pull/587?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-b3BlcmF0b3JzL3BtLmdv) | `91.89% <0.00%> (ø)` | |
| ... and [1 more](https://codecov.io/gh/corazawaf/coraza/pull/587?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) | |

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/587?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/actions/append.go b/actions/append.go
index 7b75b13..7b57030 100644
--- a/actions/append.go
+++ b/actions/append.go
@@ -27,7 +27,8 @@ func (a *appendFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
 		return
 	}
 	data := a.data.Expand(tx)
-	if _, err := tx.ResponseBodyWriter().Write([]byte(data)); err != nil {
+	// TODO: elaborate on ContentInjection actions considering that the WAF may NOT buffer the whole response
+	if it, _, err := tx.WriteResponseBody([]byte(data)); it != nil || err != nil {
 		tx.DebugLogger().Error("append failed to write to response buffer: %s", err.Error())
 	}
 }
diff --git a/actions/prepend.go b/actions/prepend.go
index 48fa606..0d403ad 100644
--- a/actions/prepend.go
+++ b/actions/prepend.go
@@ -35,15 +35,15 @@ func (a *prependFn) Evaluate(r rules.RuleMetadata, txS rules.TransactionState) {
 	data := a.data.Expand(tx)
 	buf := corazawaf.NewBodyBuffer(types.BodyBufferOptions{
 		TmpPath:     tx.WAF.TmpDir,
-		MemoryLimit: tx.WAF.RequestBodyInMemoryLimit,
-		Limit:       tx.WAF.RequestBodyLimit,
+		MemoryLimit: tx.WAF.ResponseBodyLimit,
+		Limit:       tx.WAF.ResponseBodyLimit,
 	})
 
 	_, err := buf.Write([]byte(data))
 	if err != nil {
 		tx.WAF.Logger.Debug("failed to write buffer while evaluating prepend action: %s", err.Error())
 	}
-	reader, err := tx.ResponseBodyBuffer.Reader()
+	reader, err := tx.ResponseBodyReader()
 	if err != nil {
 		tx.WAF.Logger.Debug("failed to read response body while evaluating prepend action: %s", err.Error())
 	}
@@ -51,8 +51,11 @@ func (a *prependFn) Evaluate(r rules.RuleMetadata, txS rules.TransactionState) {
 	if err != nil {
 		tx.WAF.Logger.Debug("failed to append response buffer while evaluating prepend action: %s", err.Error())
 	}
+	// TODO: elaborate on ContentInjection actions considering that the WAF may NOT buffer the whole response
 	// We overwrite the response body buffer with the new buffer
-	*tx.ResponseBodyBuffer = *buf
+	// TODO: implement the swap
+	// *tx.ResponseBodyBuffer = *buf
+
 	// Maybe in the future we could add the prepend function to the BodyBuffer
 }
 
diff --git a/http/interceptor.go b/http/interceptor.go
index 3ed2cc0..7165d38 100644
--- a/http/interceptor.go
+++ b/http/interceptor.go
@@ -27,6 +27,10 @@ type rwInterceptor struct {
 
 func (i *rwInterceptor) WriteHeader(statusCode int) {
 	if i.hasStatusCode {
+		if i.tx.IsInterrupted() {
+			// If we already set a status code, but phase4 raised an interruption, let's change it
+			i.statusCode = statusCode
+		}
 		return
 	}
 
@@ -47,7 +51,6 @@ func (i *rwInterceptor) Write(b []byte) (int, error) {
 	if !i.hasStatusCode {
 		i.WriteHeader(http.StatusOK)
 	}
-
 	if i.tx.IsInterrupted() {
 		// if there is an interruption it must be from phase 4 and hence
 		// we won't write anything to either the body or the buffer.
@@ -57,10 +60,13 @@ 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)
+		it, n, err := i.tx.WriteResponseBody(b)
+		if it != nil {
+			i.WriteHeader(http.StatusForbidden)
+			return 0, nil
+		}
+		return n, err
 	}
-
 	return i.w.Write(b)
 }
 
diff --git a/internal/corazawaf/body_buffer.go b/internal/corazawaf/body_buffer.go
index 1a09c48..8ce4414 100644
--- a/internal/corazawaf/body_buffer.go
+++ b/internal/corazawaf/body_buffer.go
@@ -51,7 +51,7 @@ func (br *BodyBuffer) Write(data []byte) (n int, err error) {
 		return 0, nil
 	}
 
-	if br.length == br.options.Limit || br.length >= (br.options.Limit-int64(len(data))) {
+	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()
diff --git a/internal/corazawaf/transaction.go b/internal/corazawaf/transaction.go
index 5c9ce8a..fcfde16 100644
--- a/internal/corazawaf/transaction.go
+++ b/internal/corazawaf/transaction.go
@@ -70,7 +70,7 @@ type Transaction struct {
 	requestBodyBuffer *BodyBuffer
 
 	// Handles response body buffers
-	ResponseBodyBuffer *BodyBuffer
+	responseBodyBuffer *BodyBuffer
 
 	// Body processor used to parse JSON, XML, etc
 	bodyProcessor bodyprocessors.BodyProcessor
@@ -324,7 +324,7 @@ func (tx *Transaction) DebugLogger() loggers.DebugLogger {
 }
 
 func (tx *Transaction) ResponseBodyReader() (io.Reader, error) {
-	return tx.ResponseBodyBuffer.Reader()
+	return tx.responseBodyBuffer.Reader()
 }
 
 func (tx *Transaction) RequestBodyReader() (io.Reader, error) {
@@ -773,7 +773,6 @@ 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,
@@ -818,6 +817,7 @@ func (tx *Transaction) WriteRequestBody(b []byte) (*types.Interruption, int, err
 	}
 
 	if tx.requestBodyBuffer.length+writingBytes >= tx.RequestBodyLimit {
+		tx.variables.inboundErrorData.Set("1")
 		if tx.WAF.RequestBodyLimitAction == types.BodyLimitActionReject {
 			// We interrupt this transaction in case RequestBodyLimitAction is Reject
 			return setAndReturnBodyLimitInterruption(tx)
@@ -883,6 +883,7 @@ func (tx *Transaction) ReadRequestBodyFrom(r io.Reader) (*types.Interruption, in
 			return nil, 0, errors.New("Overflow reached while writing request body")
 		}
 		if tx.requestBodyBuffer.length+writingBytes >= tx.RequestBodyLimit {
+			tx.variables.inboundErrorData.Set("1")
 			if tx.WAF.RequestBodyLimitAction == types.BodyLimitActionReject {
 				return setAndReturnBodyLimitInterruption(tx)
 			}
@@ -919,10 +920,10 @@ func (tx *Transaction) ReadRequestBodyFrom(r io.Reader) (*types.Interruption, in
 	return tx.interruption, int(w), err
 }
 
-// ProcessRequestBody Performs the request body (if any)
+// ProcessRequestBody Performs the analysis of the request body (if any)
 //
 // This method perform the analysis on the request body. It is optional to
-// call that function. If this API consumer already know that there isn't a
+// call that function. If this API consumer already knows that there isn't a
 // body for inspect it is recommended to skip this step.
 //
 // Remember to check for a possible intervention.
@@ -1033,14 +1034,128 @@ func (tx *Transaction) IsResponseBodyProcessable() bool {
 	return stringsutil.InSlice(ct, tx.WAF.ResponseBodyMimeTypes)
 }
 
-func (tx *Transaction) ResponseBodyWriter() io.Writer {
-	return tx.ResponseBodyBuffer
+// WriteResponseBody writes bytes from a slice of bytes into the response body,
+// it returns an interruption if the writing bytes go beyond the response body limit.
+// It won't copy the bytes if the body access isn't accessible.
+func (tx *Transaction) WriteResponseBody(b []byte) (*types.Interruption, int, error) {
+	if tx.RuleEngine == types.RuleEngineOff {
+		return nil, 0, nil
+	}
+
+	if !tx.ResponseBodyAccess {
+		return nil, 0, nil
+	}
+
+	if tx.ResponseBodyLimit == tx.responseBodyBuffer.length {
+		// tx.ResponseBodyLimit will never be zero so if this happened, we have an
+		// interruption for sure.
+		if tx.WAF.ResponseBodyLimitAction == types.BodyLimitActionReject {
+			return tx.interruption, 0, nil
+		}
+
+		if tx.WAF.ResponseBodyLimitAction == types.BodyLimitActionProcessPartial {
+			return nil, 0, nil
+		}
+	}
+
+	var (
+		writingBytes           = int64(len(b))
+		runProcessResponseBody = false
+	)
+	if tx.responseBodyBuffer.length+writingBytes >= tx.ResponseBodyLimit {
+		// TODO: figure out ErrorData vs DataError: https://github.com/corazawaf/coraza/issues/564
+		tx.variables.outboundDataError.Set("1")
+		if tx.WAF.ResponseBodyLimitAction == types.BodyLimitActionReject {
+			// We interrupt this transaction in case ResponseBodyLimitAction is Reject
+			return setAndReturnBodyLimitInterruption(tx)
+		}
+
+		if tx.WAF.ResponseBodyLimitAction == types.BodyLimitActionProcessPartial {
+			writingBytes = tx.ResponseBodyLimit - tx.responseBodyBuffer.length
+			runProcessResponseBody = true
+		}
+	}
+	w, err := tx.responseBodyBuffer.Write(b[:writingBytes])
+	if err != nil {
+		return nil, 0, err
+	}
+
+	if runProcessResponseBody {
+		_, err = tx.ProcessResponseBody()
+	}
+	return tx.interruption, int(w), err
+}
+
+// ReadResponseBodyFrom writes bytes from a reader into the response body
+// it returns an interruption if the writing bytes go beyond the response body limit.
+// It won't read the reader if the body access isn't accessible.
+func (tx *Transaction) ReadResponseBodyFrom(r io.Reader) (*types.Interruption, int, error) {
+	if tx.RuleEngine == types.RuleEngineOff {
+		return nil, 0, nil
+	}
+
+	if !tx.ResponseBodyAccess {
+		return nil, 0, nil
+	}
+
+	if tx.ResponseBodyLimit == tx.responseBodyBuffer.length {
+		if tx.WAF.ResponseBodyLimitAction == types.BodyLimitActionReject {
+			return tx.interruption, 0, nil
+		}
+
+		if tx.WAF.ResponseBodyLimitAction == types.BodyLimitActionProcessPartial {
+			return nil, 0, nil
+		}
+	}
+
+	var (
+		writingBytes           int64
+		runProcessResponseBody = false
+	)
+	if l, ok := r.(ByteLenger); ok {
+		writingBytes = int64(l.Len())
+		if tx.responseBodyBuffer.length+writingBytes >= tx.ResponseBodyLimit {
+			// TODO: figure out ErrorData vs DataError: https://github.com/corazawaf/coraza/issues/564
+			tx.variables.outboundDataError.Set("1")
+			if tx.WAF.ResponseBodyLimitAction == types.BodyLimitActionReject {
+				return setAndReturnBodyLimitInterruption(tx)
+			}
+
+			if tx.WAF.ResponseBodyLimitAction == types.BodyLimitActionProcessPartial {
+				writingBytes = tx.ResponseBodyLimit - tx.responseBodyBuffer.length
+				runProcessResponseBody = true
+			}
+		}
+	} else {
+		writingBytes = tx.ResponseBodyLimit - tx.responseBodyBuffer.length
+	}
+
+	w, err := io.CopyN(tx.responseBodyBuffer, r, writingBytes)
+	if err != nil && err != io.EOF {
+		return nil, int(w), err
+	}
+
+	if tx.responseBodyBuffer.length == tx.ResponseBodyLimit {
+		if tx.WAF.ResponseBodyLimitAction == types.BodyLimitActionReject {
+			return setAndReturnBodyLimitInterruption(tx)
+		}
+
+		if tx.WAF.ResponseBodyLimitAction == types.BodyLimitActionProcessPartial {
+			runProcessResponseBody = true
+		}
+	}
+
+	err = nil
+	if runProcessResponseBody {
+		_, err = tx.ProcessResponseBody()
+	}
+	return tx.interruption, int(w), err
 }
 
-// ProcessResponseBody Perform the response body (if any)
+// ProcessResponseBody Perform the analysis of the 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
+// This method perform the analysis on the response body. It is optional to
+// call that method. If this API consumer already knows that there isn't a
 // body for inspect it is recommended to skip this step.
 //
 // note Remember to check for a possible intervention.
@@ -1066,21 +1181,17 @@ func (tx *Transaction) ProcessResponseBody() (*types.Interruption, error) {
 		return tx.interruption, nil
 	}
 	tx.WAF.Logger.Debug("[%s] Attempting to process response body", tx.id)
-	reader, err := tx.ResponseBodyBuffer.Reader()
+	reader, err := tx.responseBodyBuffer.Reader()
 	if err != nil {
 		return tx.interruption, err
 	}
-	reader = io.LimitReader(reader, tx.WAF.ResponseBodyLimit)
+
 	buf := new(strings.Builder)
 	length, err := io.Copy(buf, reader)
 	if err != nil {
 		return tx.interruption, err
 	}
 
-	if tx.ResponseBodyBuffer.Size() >= tx.WAF.ResponseBodyLimit {
-		tx.variables.outboundDataError.Set("1")
-	}
-
 	tx.variables.responseContentLength.Set(strconv.FormatInt(length, 10))
 	tx.variables.responseBody.Set(buf.String())
 	tx.WAF.Rules.Eval(types.PhaseResponseBody, tx)
@@ -1266,7 +1377,7 @@ func (tx *Transaction) Close() error {
 	if err := tx.requestBodyBuffer.Reset(); err != nil {
 		errs = append(errs, err)
 	}
-	if err := tx.ResponseBodyBuffer.Reset(); err != nil {
+	if err := tx.responseBodyBuffer.Reset(); err != nil {
 		errs = append(errs, err)
 	}
 
diff --git a/internal/corazawaf/waf.go b/internal/corazawaf/waf.go
index d6dd66d..6cca23c 100644
--- a/internal/corazawaf/waf.go
+++ b/internal/corazawaf/waf.go
@@ -177,7 +177,7 @@ func (w *WAF) newTransactionWithID(id string) *Transaction {
 			MemoryLimit: w.RequestBodyInMemoryLimit,
 			Limit:       w.ResponseBodyLimit,
 		})
-		tx.ResponseBodyBuffer = NewBodyBuffer(types.BodyBufferOptions{
+		tx.responseBodyBuffer = NewBodyBuffer(types.BodyBufferOptions{
 			TmpPath: w.TmpDir,
 			// the response body is just buffered in memory. Therefore, Limit and MemoryLimit are equal.
 			MemoryLimit: w.ResponseBodyLimit,
diff --git a/internal/seclang/directives.go b/internal/seclang/directives.go
index de3a65b..6d0052f 100644
--- a/internal/seclang/directives.go
+++ b/internal/seclang/directives.go
@@ -92,11 +92,12 @@ func directiveSecMarker(options *DirectiveOptions) error {
 }
 
 // Description: Unconditionally processes the action list it receives as the first and only parameter.
-// Syntax: SecAction "action1,action2,action3,...
+// Syntax: SecAction "action1,action2,action3,..."
+// ----
 // This directive is commonly used to set variables and initialize persistent collections using the initcol action.
+// The syntax of the parameter is identical to that of the third parameter of SecRule. For example:
 //
-//	The syntax of the parameter is identical to that of the third parameter of SecRule. For example:
-//
+// Example:
 // ```
 // SecAction "nolog,phase:1,initcol:RESOURCE=%{REQUEST_FILENAME}"
 // ```
@@ -118,6 +119,18 @@ func directiveSecAction(options *DirectiveOptions) error {
 	return nil
 }
 
+// Description: Creates a rule that will analyze the selected variables using
+// the selected operator.
+// Syntax: SecRule VARIABLES OPERATOR [ACTIONS]
+// ---
+// Example: SecRule ARGS "@rx attack" "phase:1,log,deny,id:1"
+//
+// Every rule must provide one or more variables along with the operator that should
+// be used to inspect them. If no actions are provided, the default list will be used.
+// (There is always a default list, even if one was not explicitly set with SecDefaultAction.)
+// If there are actions specified in a rule, they will be merged with the default list
+// to form the final actions that will be used. (The actions in the rule will overwrite
+// those in the default list.) Refer to SecDefaultAction for more information.
 func directiveSecRule(options *DirectiveOptions) error {
 	ignoreErrors := options.Config.Get("ignore_rule_compilation_errors", false).(bool)
 	rule, err := ParseRule(RuleOptions{
@@ -143,6 +156,14 @@ func directiveSecRule(options *DirectiveOptions) error {
 	return nil
 }
 
+// Description: Configures whether response bodies are to be buffered.
+// Syntax: SecResponseBodyAccess On|Off
+// Default: Off
+// This directive is required if you plan to inspect HTML responses and implement
+// response blocking. Possible values are:
+// - On: buffer response bodies (but only if the response MIME type matches the list
+// configured with SecResponseBodyMimeType).
+// - Off: do not buffer response bodies.
 func directiveSecResponseBodyAccess(options *DirectiveOptions) error {
 	b, err := parseBoolean(strings.ToLower(options.Opts))
 	if err != nil {
@@ -168,7 +189,9 @@ func directiveSecRequestBodyLimit(options *DirectiveOptions) error {
 // Default: Off
 // Syntax: SecRequestBodyAccess On|Off
 // ---
-// This directive is required if you want to inspect the data transported request bodies (e.g., POST parameters). Request buffering is also required in order to make reliable blocking possible. The possible values are:
+// This directive is required if you want to inspect the data transported request bodies
+// (e.g., POST parameters). Request buffering is also required in order to make reliable
+// blocking possible. The possible values are:
 // - On: buffer request bodies
 // - Off: do not buffer request bodies
 func directiveSecRequestBodyAccess(options *DirectiveOptions) error {
@@ -180,36 +203,15 @@ func directiveSecRequestBodyAccess(options *DirectiveOptions) error {
 	return nil
 }
 
-// directiveSecAuditEngine declares the SecAuditEngine directive
-//
-// Description: Configures the audit logging engine.
+// Description: Configures the rules engine.
+// Syntax: SecRuleEngine On|Off|DetectionOnly
 // Default: Off
-// Version Compatibility: 1.0+
-// Syntax: SecAuditEngine RelevantOnly
-//
-// -----
-//
-// The SecAuditEngine directive is used to configure the audit engine, which logs complete
-// transactions. Coraza is currently able to log most, but not all transactions. Transactions
-// involving errors (e.g., 400 and 404 transactions) use a different execution path, which
-// Coraza does not support.
-//
-// The possible values for the audit log engine are as follows:
-//
-// - **On:** log all transactions
-// - **Off:** do not log any transactions
-// - **RelevantOnly:** only the log transactions that have triggered a warning or an error, or have a status code that is considered to be relevant (as determined by the SecAuditLogRelevantStatus directive)
-//
-// Note : If you need to change the audit log engine configuration on a per-transaction basis (e.g., in response to some transaction data), use the ctl action. The following example demonstrates how SecAuditEngine is used:
-//
-// ```apache
-// SecAuditEngine RelevantOnly
-// SecAuditLog logs/audit/audit.log
-// SecAuditLogParts ABCFHZ
-// SecAuditLogType concurrent
-// SecAuditLogStorageDir logs/audit
-// SecAuditLogRelevantStatus ^(?:5|4(?!04))
-// ```
+// ---
+// The possible values are:
+// - On: process rules
+// - Off: do not process rules
+// - DetectionOnly: process rules but never executes any disruptive actions
+// (block, deny, drop, allow, proxy and redirect)
 func directiveSecRuleEngine(options *DirectiveOptions) error {
 	engine, err := types.ParseRuleEngineStatus(options.Opts)
 	options.WAF.RuleEngine = engine
@@ -265,6 +267,23 @@ func directiveSecResponseBodyMimeType(options *DirectiveOptions) error {
 	return nil
 }
 
+// Description: Controls what happens once a response body limit, configured with
+// SecResponseBodyLimit, is encountered.
+// Syntax: SecResponseBodyLimitAction Reject|ProcessPartial
+// ---
+// By default, Coraza will reject a response body that is longer than specified.
+// Some web sites, however, will produce very long responses, making it difficult
+// to come up with a reasonable limit. Such sites would have to raise the limit
+// significantly to function properly, defying the purpose of having the limit in
+// the first place (to control memory consumption). With the ability to choose what
+// happens once a limit is reached, site administrators can choose to inspect only
+// the first part of the response, the part that can fit into the desired limit, and
+// let the rest through. Some could argue that allowing parts of responses to go
+// uninspected is a weakness. This is true in theory, but applies only to cases in
+// which the attacker controls the output (e.g., can make it arbitrary long). In such
+// cases, however, it is not possible to prevent leakage anyway. The attacker could
+// compress, obfuscate, or even encrypt data before it is sent back, and therefore
+// bypass any monitoring device.
 func directiveSecResponseBodyLimitAction(options *DirectiveOptions) error {
 	switch strings.ToLower(options.Opts) {
 	case "reject":
@@ -277,12 +296,24 @@ func directiveSecResponseBodyLimitAction(options *DirectiveOptions) error {
 	return nil
 }
 
+// Description: Configures the maximum response body size that will be accepted for buffering.
+// Syntax: SecResponseBodyLimit [LIMIT_IN_BYTES]
+// Default: 524288 (512 KB)
+// ---
+// Anything over this limit will be rejected with status code 500 (Internal Server Error).
+// This setting will not affect the responses with MIME types that are not selected for
+// buffering. There is a hard limit of 1 GB.
 func directiveSecResponseBodyLimit(options *DirectiveOptions) error {
 	var err error
 	options.WAF.ResponseBodyLimit, err = strconv.ParseInt(options.Opts, 10, 64)
 	return err
 }
 
+// Description: Controls what happens once a request body limit, configured with
+// SecRequestBodyLimit, is encountered
+// Syntax: SecRequestBodyLimitAction Reject|ProcessPartial
+// ---
+// By default, Coraza will reject a request body that is longer than specified.
 func directiveSecRequestBodyLimitAction(options *DirectiveOptions) error {
 	switch strings.ToLower(options.Opts) {
 	case "reject":
@@ -381,7 +412,6 @@ func directiveSecHashEngine(options *DirectiveOptions) error {
 // the connection.
 //
 // Important: Every SecDefaultAction directive must specify a disruptive action and a processing phase and cannot contain metadata actions.
-
 func directiveSecDefaultAction(options *DirectiveOptions) error {
 	da, _ := options.Config.Get("rule_default_actions", []string{}).([]string)
 	da = append(da, options.Opts)
@@ -462,6 +492,10 @@ func directiveSecAuditLogType(options *DirectiveOptions) error {
 	return nil
 }
 
+// Description: Select the output format of the AuditLogs. The format can be either
+// the native AuditLogs format or JSON.
+// Syntax: SecAuditLogFormat JSON|Native
+// Default: Native
 func directiveSecAuditLogFormat(options *DirectiveOptions) error {
 	if len(options.Opts) == 0 {
 		return errors.New("syntax error: SecAuditLogFormat [json/native/...]")
@@ -488,6 +522,17 @@ func directiveSecAuditLogDir(options *DirectiveOptions) error {
 	return nil
 }
 
+// Description: Configures the mode (permissions) of any directories created for the
+// concurrent audit logs, using an octal mode value as parameter (as used in chmod).
+// Syntax: SecAuditLogDirMode octal_mode|"default"
+// Default: 0600
+// ---
+// The default mode for new audit log directories (0600) only grants read/write access
+// to the owner.
+// Example:
+// ```apache
+// SecAuditLogDirMode 02750
+// ```
 func directiveSecAuditLogDirMode(options *DirectiveOptions) error {
 	if len(options.Opts) == 0 {
 		return errors.New("syntax error: SecAuditLogDirMode [0777/0700/...]")
@@ -503,6 +548,16 @@ func directiveSecAuditLogDirMode(options *DirectiveOptions) error {
 	return nil
 }
 
+// Description: Configures the mode (permissions) of any files created for concurrent
+// audit logs using an octal mode (as used in chmod). See SecAuditLogDirMode for
+// controlling the mode of created audit log directories.
+// Syntax: SecAuditLogFileMode octal_mode|"default"
+// Default: 0600
+// ---
+// Example:
+// ```apache
+// SecAuditLogFileMode 00640
+// ```
 func directiveSecAuditLogFileMode(options *DirectiveOptions) error {
 	if len(options.Opts) == 0 {
 		return errors.New("syntax error: SecAuditLogFileMode [0777/0700/...]")
@@ -545,35 +600,59 @@ func directiveSecAuditLogRelevantStatus(options *DirectiveOptions) error {
 	return err
 }
 
-func directiveSecAuditLogParts(options *DirectiveOptions) error {
-	options.WAF.AuditLogParts = types.AuditLogParts(options.Opts)
-	return nil
-}
-
-// directiveSecAuditEngine declares the SecAuditEngine directive
-//
-// Description: Defines which parts of each transaction are going to be recorded in
-// the audit log. Each part is assigned a single letter; when a letter appears in the
-// list then the equivalent part will be recorded. See below for the list of all parts.
-// Default: ABCFHZ
-// Syntax: SecAuditLogParts ABCFHZ
+// Description: Defines which parts of each transaction are going to be recorded
+// in the audit log. Each part is assigned a single letter; when a letter appears
+// in the list then the equivalent part will be recorded. See below for the list of
+// all parts.
+// Syntax: SecAuditLogParts [PARTLETTERS]
+// Default: ABCFHZ Note
 // ---
-// The format of the audit log format is documented in detail in the logging page.
+// The format of the audit log format is documented in detail in the Audit Log Data Format Documentation.
+// Example:
+// ```
+// SecAuditLogParts ABCFHZ
+// ```
 //
 // Available audit log parts:
 //
 // A: Audit log header (mandatory).
 // B: Request headers.
-// C: Request body (present only if the request body exists and Coraza is configured to intercept it. This would require SecRequestBodyAccess to be set to on).
+// C: Request body (present only if the request body exists and ModSecurity is configured to intercept it. This would require SecRequestBodyAccess to be set to on).
 // D: Reserved for intermediary response headers; not implemented yet.
-// E: Intermediary response body (present only if Coraza is configured to intercept response bodies, and if the audit log engine is configured to record it. Intercepting response bodies requires SecResponseBodyAccess to be enabled). Intermediary response body is the same as the actual response body unless Coraza intercepts the intermediary response body, in which case the actual response body will contain the error message (either the Apache default error message, - orthe ErrorDocument page).
+// E: Intermediary response body (present only if ModSecurity is configured to intercept response bodies, and if the audit log engine is configured to record it. Intercepting response bodies requires SecResponseBodyAccess to be enabled). Intermediary response body is the same as the actual response body unless ModSecurity intercepts the intermediary response body, in which case the actual response body will contain the error message (either the Apache default error message, or the ErrorDocument page).
 // F: Final response headers (excluding the Date and Server headers, which are always added by Apache in the late stage of content delivery).
 // G: Reserved for the actual response body; not implemented yet.
 // H: Audit log trailer.
 // I: This part is a replacement for part C. It will log the same data as C in all cases except when multipart/form-data encoding in used. In this case, it will log a fake application/x-www-form-urlencoded body that contains the information about parameters but not about the files. This is handy if you don’t want to have (often large) files stored in your audit logs.
 // J: This part contains information about the files uploaded using multipart/form-data encoding.
-// K: This part contains a full list of every rule that matched (one per line) in the order they were matched. The rules are fully qualified and will thus show inherited actions and default operators.
+// K: This part contains a full list of every rule that matched (one per line) in the order they were matched. The rules are fully qualified and will thus show inherited actions and default operators. Supported as of v2.5.0.
 // Z: Final boundary, signifies the end of the entry (mandatory).
+func directiveSecAuditLogParts(options *DirectiveOptions) error {
+	options.WAF.AuditLogParts = types.AuditLogParts(options.Opts)
+	return nil
+}
+
+// Description: Configures the audit logging engine.
+// Syntax: SecAuditEngine RelevantOnly
+// Default: Off
+// ---
+// The SecAuditEngine directive is used to configure the audit engine, which logs complete transactions. ModSecurity is currently able to log most, but not all transactions. Transactions involving errors (e.g., 400 and 404 transactions) use a different execution path, which ModSecurity does not support.
+//
+// The possible values for the audit log engine are as follows:
+// - On: log all transactions
+// - Off: do not log any transactions
+// - RelevantOnly: only the log transactions that have triggered a warning or an error, or have a status code that is considered to be relevant (as determined by the SecAuditLogRelevantStatus directive)
+//
+// Note: If you need to change the audit log engine configuration on a per-transaction basis (e.g., in response to some transaction data), use the ctl action.
+// The following example demonstrates how SecAuditEngine is used:
+// ```apache
+// SecAuditEngine RelevantOnly
+// SecAuditLog logs/audit/audit.log
+// SecAuditLogParts ABCFHZ
+// SecAuditLogType concurrent
+// SecAuditLogStorageDir logs/audit
+// SecAuditLogRelevantStatus ^(?:5|4(?!04))
+// ```
 func directiveSecAuditEngine(options *DirectiveOptions) error {
 	au, err := types.ParseAuditEngineStatus(options.Opts)
 	options.WAF.AuditEngine = au
@@ -634,9 +713,10 @@ func directiveSecRequestBodyNoFilesLimit(options *DirectiveOptions) error {
 }
 
 // Description: Path to the Coraza debug log file.
-// Syntax: SecDebugLog /path/to/coraza-debug.log
+// Syntax: SecDebugLog [ABSOLUTE_PATH_TO_DEBUG_LOG]
 // ---
-// Logs will be written to this file. Make sure the process user has write access to the directory.
+// Logs will be written to this file. Make sure the process user has write access to the
+// directory.
 func directiveSecDebugLog(options *DirectiveOptions) error {
 	return options.WAF.SetDebugLogPath(options.Opts)
 }
diff --git a/rules/transaction.go b/rules/transaction.go
index bff517e..eccf774 100644
--- a/rules/transaction.go
+++ b/rules/transaction.go
@@ -26,9 +26,12 @@ type TransactionState interface {
 	// Interrupt interrupts the transaction.
 	Interrupt(interruption *types.Interruption)
 
+	// TODO: elaborate while addressing ContentInjection actions
 	// ResponseBodyWriter allows writing to the response body.
 	// TODO(anuraaga): Should this be combined with interruption? Any action writing anything to response can be dangerous.
-	ResponseBodyWriter() io.Writer
+	// ResponseBodyWriter() io.Writer
+	WriteResponseBody(b []byte) (*types.Interruption, int, error)
+	ReadResponseBodyFrom(io.Reader) (*types.Interruption, int, error)
 
 	// ContentInjection returns whether content injection is enabled for this transaction.
 	ContentInjection() bool // TODO(anuraaga): Should be resolved at Init time when WAF is truly immutable.
diff --git a/types/transaction.go b/types/transaction.go
index 04068dc..ec1640c 100644
--- a/types/transaction.go
+++ b/types/transaction.go
@@ -72,13 +72,13 @@ type Transaction interface {
 	// This will set ARGS_(GET|POST), ARGS, ARGS_NAMES, ARGS_COMBINED_SIZE and
 	// ARGS_(GET|POST)_NAMES
 	// With this method it is possible to feed Coraza with a GET or POST argument
-	// providing granual control over the arguments.
+	// providing granular control over the arguments.
 	AddArgument(orig ArgumentType, key string, value string)
 
-	// ProcessRequestBody Performs the request body (if any)
+	// ProcessRequestBody Performs the analysis of the request body (if any)
 	//
 	// This method perform the analysis on the request body. It is optional to
-	// call that function. If this API consumer already know that there isn't a
+	// call that function. If this API consumer already knows that there isn't a
 	// body for inspect it is recommended to skip this step.
 	//
 	// Remember to check for a possible intervention.
@@ -113,24 +113,36 @@ type Transaction interface {
 	// note: Remember to check for a possible intervention.
 	ProcessResponseHeaders(code int, proto string) *Interruption
 
-	// ResponseBodyWriter returns a io.Writer for writing the response body to.
-	// Contents will be buffered until the transaction is closed.
-	ResponseBodyWriter() io.Writer
-
 	// ResponseBodyReader returns a reader for content that has been written by
-	// ResponseBodyWriter. This can be useful for buffering the response body
+	// response body buffer. This can be useful for buffering the response body
 	// within the Transaction while also passing it further in an HTTP framework.
 	ResponseBodyReader() (io.Reader, error)
 
-	// ProcessResponseBody Perform the request body (if any)
+	// ProcessResponseBody Perform the analysis of 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
+	// This method perform the analysis on the response body. It is optional to
+	// call that method. If this API consumer already knows that there isn't a
 	// body for inspect it is recommended to skip this step.
 	//
 	// note Remember to check for a possible intervention.
 	ProcessResponseBody() (*Interruption, error)
 
+	// WriteResponseBody attempts to write data into the body up to the buffer limit and
+	// returns an interruption if the body is bigger than the limit and the action is to
+	// reject. This is specially convenient to resolve an interruption before copying
+	// the body into the response body buffer.
+	//
+	// It returns the corresponding interruption, the number of bytes written an error if any.
+	WriteResponseBody(b []byte) (*Interruption, int, error)
+
+	// ReadResponseBodyFrom attempts to write data into the body up to the buffer limit and
+	// returns an interruption if the body is bigger than the limit and the action is to
+	// reject. This is specially convenient to resolve an interruption before copying
+	// the body into the response body buffer.
+	//
+	// It returns the corresponding interruption, the number of bytes written an error if any.
+	ReadResponseBodyFrom(io.Reader) (*Interruption, int, error)
+
 	// ProcessLogging Logging all information relative to this transaction.
 	// An error log
 	// At this point there is not need to hold the connection, the response can be
diff --git a/types/waf.go b/types/waf.go
index c18860b..1b3c37f 100644
--- a/types/waf.go
+++ b/types/waf.go
@@ -97,17 +97,16 @@ func (re RuleEngineStatus) String() string {
 	return "unknown"
 }
 
-// BodyLimitAction represents the action
-// to take when the request body size exceeds
-// the configured limit.
+// BodyLimitAction represents the action to take when
+// the body size exceeds the configured limit.
 type BodyLimitAction int
 
 const (
-	// BodyLimitActionProcessPartial will process the request body
-	// up to the limit and then reject the request
+	// BodyLimitActionProcessPartial will process the body
+	// up to the limit and then ignores the remaining body bytes
 	BodyLimitActionProcessPartial BodyLimitAction = 0
-	// BodyLimitActionReject will reject the request in case
-	// the request body size exceeds the configured limit
+	// BodyLimitActionReject will reject the connection in case
+	// the body size exceeds the configured limit
 	BodyLimitActionReject BodyLimitAction = 1
 )
 


Test patch:
diff --git a/examples/http-server/main_test.go b/examples/http-server/main_test.go
index 78d9d65..abb8c21 100644
--- a/examples/http-server/main_test.go
+++ b/examples/http-server/main_test.go
@@ -76,17 +76,16 @@ func TestHttpServer(t *testing.T) {
 			},
 			[]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,
-		// },
+		{
+			"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 {
diff --git a/internal/corazawaf/transaction_test.go b/internal/corazawaf/transaction_test.go
index ce3c929..3e13ba4 100644
--- a/internal/corazawaf/transaction_test.go
+++ b/internal/corazawaf/transaction_test.go
@@ -429,22 +429,212 @@ func TestAuditLog(t *testing.T) {
 	}
 }
 
-func TestResponseBody(t *testing.T) {
-	tx := makeTransaction(t)
-	tx.ResponseBodyAccess = true
-	tx.RuleEngine = types.RuleEngineOn
-	tx.AddResponseHeader("content-type", "text/plain")
-	if _, err := tx.ResponseBodyBuffer.Write([]byte("test123")); err != nil {
-		t.Error("Failed to write response body buffer")
+var responseBodyWriters = map[string]func(tx *Transaction, body string) (*types.Interruption, int, error){
+	"WriteResponsequestBody": func(tx *Transaction, body string) (*types.Interruption, int, error) {
+		return tx.WriteResponseBody([]byte(body))
+	},
+	"ReadResponseBodyFromKnownLen": func(tx *Transaction, body string) (*types.Interruption, int, error) {
+		return tx.ReadResponseBodyFrom(strings.NewReader(body))
+	},
+	"ReadResponseBodyFromUnknownLen": func(tx *Transaction, body string) (*types.Interruption, int, error) {
+		return tx.ReadResponseBodyFrom(struct{ io.Reader }{
+			strings.NewReader(body),
+		})
+	},
+}
+
+func TestWriteResponseBody(t *testing.T) {
+	const (
+		urlencodedBody    = "some=result&second=data"
+		urlencodedBodyLen = len(urlencodedBody)
+	)
+
+	testCases := []struct {
+		name                    string
+		responseBodyLimit       int
+		responseBodyLimitAction types.BodyLimitAction
+		shouldInterrupt         bool
+	}{
+		{
+			name:                    "LimitNotReached",
+			responseBodyLimit:       urlencodedBodyLen + 2,
+			responseBodyLimitAction: types.BodyLimitAction(-1),
+		},
+		{
+			name:                    "LimitReachedAndRejects",
+			responseBodyLimit:       urlencodedBodyLen - 3,
+			responseBodyLimitAction: types.BodyLimitActionReject,
+			shouldInterrupt:         true,
+		},
+		{
+			name:                    "LimitReachedAndPartialProcessing",
+			responseBodyLimit:       urlencodedBodyLen - 3,
+			responseBodyLimitAction: types.BodyLimitActionProcessPartial,
+		},
 	}
-	if _, err := tx.ProcessResponseBody(); err != nil {
-		t.Error("Failed to process response body")
+
+	urlencodedBodyLenThird := urlencodedBodyLen / 3
+	bodyChunks := map[string][]string{
+		"BodyInOneShot":     {urlencodedBody},
+		"BodyInThreeChunks": {urlencodedBody[0:urlencodedBodyLenThird], urlencodedBody[urlencodedBodyLenThird : 2*urlencodedBodyLenThird], urlencodedBody[2*urlencodedBodyLenThird:]},
 	}
-	if tx.variables.responseBody.String() != "test123" {
-		t.Error("failed to set response body")
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			for name, writeResponseBody := range responseBodyWriters {
+				t.Run(name, func(t *testing.T) {
+					for name, chunks := range bodyChunks {
+						t.Run(name, func(t *testing.T) {
+							waf := NewWAF()
+							waf.RuleEngine = types.RuleEngineOn
+							waf.ResponseBodyAccess = true
+							waf.ResponseBodyLimit = int64(testCase.responseBodyLimit)
+							waf.ResponseBodyLimitAction = testCase.responseBodyLimitAction
+
+							tx := waf.NewTransaction()
+							tx.AddResponseHeader("content-type", "text/plain")
+
+							it := tx.ProcessResponseHeaders(200, "HTTP/1")
+							if it != nil {
+								t.Fatal("Unexpected interruption on headers")
+							}
+
+							var err error
+
+							for _, c := range chunks {
+								if it, _, err = writeResponseBody(tx, c); err != nil {
+									t.Errorf("Failed to write body buffer: %s", err.Error())
+								}
+							}
+
+							if testCase.shouldInterrupt {
+								if it == nil {
+									t.Fatal("Expected interruption, got nil")
+								}
+							} else {
+								it, err := tx.ProcessResponseBody()
+								if err != nil {
+									t.Fatal(err)
+								}
+
+								if it != nil {
+									t.Fatalf("Unexpected interruption")
+								}
+								// checking if the body has been populated up to the first POST arg
+								index := strings.Index(urlencodedBody, "&")
+								if tx.variables.responseBody.String()[:index] != urlencodedBody[:index] {
+									t.Error("failed to set response body")
+								}
+							}
+
+							_ = tx.Close()
+						})
+					}
+
+				})
+			}
+
+		})
 	}
-	if err := tx.Close(); err != nil {
-		t.Error(err)
+}
+
+func TestWriteResponseBodyOnLimitReached(t *testing.T) {
+	testCases := map[string]struct {
+		responseBodyLimitAction types.BodyLimitAction
+		preexistingInterruption *types.Interruption
+	}{
+		"reject": {
+			responseBodyLimitAction: types.BodyLimitActionReject,
+			preexistingInterruption: &types.Interruption{
+				RuleID: 123,
+			},
+		},
+		"partial processing": {
+			responseBodyLimitAction: types.BodyLimitActionProcessPartial,
+		},
+	}
+
+	for tName, tCase := range testCases {
+		waf := NewWAF()
+		waf.RuleEngine = types.RuleEngineOn
+		waf.ResponseBodyAccess = true
+		waf.ResponseBodyLimit = 2
+		waf.ResponseBodyLimitAction = tCase.responseBodyLimitAction
+
+		t.Run(tName, func(t *testing.T) {
+			for wName, writer := range responseBodyWriters {
+				t.Run(wName, func(t *testing.T) {
+					tx := waf.NewTransaction()
+					_, err := tx.responseBodyBuffer.Write([]byte("ab"))
+					if err != nil {
+						t.Fatalf("unexpected error when writing to body buffer directly: %s", err.Error())
+					}
+					tx.interruption = tCase.preexistingInterruption
+
+					it, n, err := writer(tx, "c")
+					if err != nil {
+						t.Fatalf("unexpected error: %s", err.Error())
+					}
+
+					if it != tCase.preexistingInterruption {
+						t.Fatalf("unexpected interruption")
+					}
+
+					if n != 0 {
+						t.Fatalf("unexpected number of bytes written")
+					}
+
+					_ = tx.Close()
+				})
+			}
+		})
+	}
+}
+
+func TestWriteResponseBodyIsNopWhenBodyIsNotAccesible(t *testing.T) {
+	testCases := []struct {
+		ruleEngine         types.RuleEngineStatus
+		responseBodyAccess bool
+	}{
+		{
+			ruleEngine: types.RuleEngineOff,
+		},
+		{
+			ruleEngine:         types.RuleEngineOn,
+			responseBodyAccess: false,
+		},
+	}
+
+	for _, tCase := range testCases {
+		t.Run(fmt.Sprintf(
+			"ruleEngine = %s and responseBodyAccess = %t",
+			tCase.ruleEngine.String(),
+			tCase.responseBodyAccess,
+		), func(t *testing.T) {
+			waf := NewWAF()
+			waf.RuleEngine = tCase.ruleEngine
+			waf.ResponseBodyAccess = tCase.responseBodyAccess
+
+			for wName, writer := range responseBodyWriters {
+				t.Run(wName, func(t *testing.T) {
+					tx := waf.NewTransaction()
+					it, n, err := writer(tx, "abc")
+					if err != nil {
+						t.Fatalf("unexpected error: %s", err.Error())
+					}
+
+					if it != nil {
+						t.Fatalf("unexpected interruption")
+					}
+
+					if n != 0 {
+						t.Fatalf("unexpected number of bytes written")
+					}
+
+					_ = tx.Close()
+				})
+			}
+		})
 	}
 }
 
@@ -666,31 +856,26 @@ func TestTransactionSyncPool(t *testing.T) {
 	}
 }
 
-// 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 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 it, _, err := tx.WriteResponseBody([]byte("more bytes")); it != nil || 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/rules_test.go b/internal/seclang/rules_test.go
index 6f848ec..3aa70bc 100644
--- a/internal/seclang/rules_test.go
+++ b/internal/seclang/rules_test.go
@@ -317,9 +317,10 @@ func TestTxIssue147(t *testing.T) {
 	// we need a content-type header
 	tx.AddResponseHeader("Content-Type", "text/html")
 	if tx.IsResponseBodyProcessable() {
-		if _, err := tx.ResponseBodyBuffer.Write([]byte("#!/usr/bin/python")); err != nil {
+		if it, _, err := tx.WriteResponseBody([]byte("#!/usr/bin/python")); it != nil || err != nil {
 			t.Error(err)
 		}
+
 		it, err := tx.ProcessResponseBody()
 		if err != nil {
 			t.Error(err)
diff --git a/testing/engine.go b/testing/engine.go
index 63a4fc8..9fe17aa 100644
--- a/testing/engine.go
+++ b/testing/engine.go
@@ -151,7 +151,7 @@ func (t *Test) SetResponseBody(body interface{}) error {
 	if lbody == 0 {
 		return nil
 	}
-	if _, err := t.transaction.ResponseBodyWriter().Write([]byte(data)); err != nil {
+	if it, _, err := t.transaction.WriteResponseBody([]byte(data)); it != nil || err != nil {
 		return err
 	}
 	return nil