Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 11 additions & 12 deletions internal/actions/expirevar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,62 +11,61 @@ import (

func TestExpirevar(t *testing.T) {
t.Parallel()
a, err := Get("expirevar")
if err != nil {
t.Error("failed to get setvar action")
if _, err := Get("expirevar"); err != nil {
t.Error("failed to get expirevar action")
}

t.Run("no arguments", func(t *testing.T) {
t.Parallel()
err := a.Init(nil, "")
err := expirevar().Init(nil, "")
if !errors.Is(err, ErrMissingArguments) {
t.Errorf("expected error ErrMissingArguments, got %v", err)
}
})

t.Run("invalid collection", func(t *testing.T) {
t.Parallel()
err := a.Init(nil, "INVALID.test=60")
err := expirevar().Init(nil, "INVALID.test=60")
if !strings.Contains(err.Error(), "invalid collection, supported collections are: ") {
t.Errorf("expected error 'invalid collection...', got %v", err)
}
})

t.Run("missing variable name", func(t *testing.T) {
t.Parallel()
err := a.Init(nil, "IP.=60")
err := expirevar().Init(nil, "IP.=60")
if !errors.Is(err, ErrInvalidKVArguments) {
t.Errorf("expected error ErrInvalidKVArguments, got %v", err)
}
})

t.Run("missing ttl", func(t *testing.T) {
t.Parallel()
err := a.Init(nil, "IP.test=")
err := expirevar().Init(nil, "IP.test=")
if !strings.Contains(err.Error(), "invalid TTL, must be a positive integer") {
t.Errorf("expected error 'missing TTL value', got %v", err)
}
})

t.Run("missing ttl", func(t *testing.T) {
t.Run("negative ttl", func(t *testing.T) {
t.Parallel()
err := a.Init(nil, "IP.test=-1")
err := expirevar().Init(nil, "IP.test=-1")
if !strings.Contains(err.Error(), "invalid TTL, must be a positive integer") {
t.Errorf("expected error 'missing TTL value', got %v", err)
}
})

t.Run("missing ttl", func(t *testing.T) {
t.Run("zero ttl", func(t *testing.T) {
t.Parallel()
err := a.Init(nil, "IP.test=0")
err := expirevar().Init(nil, "IP.test=0")
if !strings.Contains(err.Error(), "invalid TTL, must be a positive integer") {
t.Errorf("expected error 'missing TTL value', got %v", err)
}
})

t.Run("valid input", func(t *testing.T) {
t.Parallel()
err := a.Init(nil, "TX.testvar=60")
err := expirevar().Init(nil, "TX.testvar=60")
if err != nil {
t.Errorf("unexpected error for valid input, got %v", err)
}
Expand Down
2 changes: 2 additions & 0 deletions internal/actions/initcol_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright 2023 Juan Pablo Tosso and the OWASP Coraza contributors
// SPDX-License-Identifier: Apache-2.0

//go:build !tinygo

package actions_test

import (
Expand Down
61 changes: 61 additions & 0 deletions internal/corazawaf/body_buffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,16 @@ func (br *BodyBuffer) Write(data []byte) (n int, err error) {

type bodyBufferReader struct {
pos int
// err, when set, is returned by every Read; used to surface
// types.ErrBodyTruncated on readers invalidated by Truncate.
err error
br *BodyBuffer
}

func (b *bodyBufferReader) Read(p []byte) (n int, err error) {
if b.err != nil {
return 0, b.err
}
if b.br == nil {
// reader has been closed and hence we don't attempt to do anymore read
return 0, io.EOF
Expand Down Expand Up @@ -146,6 +152,61 @@ func (br *BodyBuffer) Size() int64 {
return br.length
}

// Truncate discards all buffered content beyond limit bytes, releasing the
// backing memory. The in-memory buffer is replaced with a fresh one holding
// only the prefix (bytes.Buffer.Truncate retains the full backing array,
// which transaction pooling would keep alive past Close); the file-backed
// buffer is truncated in place, or closed and removed when limit is 0.
// A limit at or above the current size is a no-op.
func (br *BodyBuffer) Truncate(limit int64) error {
if limit >= br.length {
return nil
}

if environment.HasAccessToFS && br.writer != nil {
if limit == 0 {
w := br.writer
br.writer = nil
br.length = 0
br.invalidateReaders()
err := w.Close()
// remove even when Close fails: br.writer is already nil, so
// this is the last reference to the file and Reset() would no
// longer clean it up.
if rmErr := os.Remove(w.Name()); err == nil {
err = rmErr
}
return err
}
if err := br.writer.Truncate(limit); err != nil {
// nothing changed: length and readers stay valid
return err
}
br.length = limit
br.invalidateReaders()
return nil
}
fresh := &bytes.Buffer{}
fresh.Write(br.buffer.Bytes()[:limit])
br.buffer = fresh
br.length = limit
br.invalidateReaders()
return nil
}

// invalidateReaders marks all outstanding readers with ErrBodyTruncated:
// reading with a position beyond a shrunk buffer would panic, and quietly
// serving the prefix or EOF would hide the contract violation of holding a
// reader across truncation. New readers (e.g. audit log part C) see the
// kept prefix.
func (br *BodyBuffer) invalidateReaders() {
for _, r := range br.readers {
r.br = nil
r.err = types.ErrBodyTruncated
}
br.readers = nil
}

// Reset will reset buffers and delete temporary files
func (br *BodyBuffer) Reset() error {
br.buffer.Reset()
Expand Down
24 changes: 24 additions & 0 deletions internal/corazawaf/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,30 @@ func (tx *Transaction) SetScriptUsername(value string) {
tx.variables.scriptUsername.Set(value)
}

// TruncateRequestBody releases the buffered request body, keeping at most
// limit bytes as a preview for audit logging. See types.Transaction for the
// full contract. The REQUEST_BODY variable is truncated to the same prefix
// (copied, so the original backing string is released); REQUEST_BODY_LENGTH
// and parsed derivatives such as ARGS_POST are left intact.
func (tx *Transaction) TruncateRequestBody(limit int64) error {
if limit < 0 {
return errors.New("limit must be non-negative")
}
// lastPhase only advances when rules are evaluated, so it stays behind
// when the engine is off or the transaction was interrupted — states in
// which request-body analysis will never run and truncation is safe.
if tx.lastPhase < types.PhaseRequestBody && !tx.IsRuleEngineOff() && !tx.IsInterrupted() {
return errors.New("TruncateRequestBody must be called after ProcessRequestBody")
}
if err := tx.requestBodyBuffer.Truncate(limit); err != nil {
return err
}
if rb := tx.variables.requestBody.Get(); int64(len(rb)) > limit {
tx.variables.requestBody.Set(strings.Clone(rb[:limit]))
}
return nil
}

func (tx *Transaction) ID() string {
return tx.id
}
Expand Down
Loading