Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 11, 19:45 UTC
logging: fix multiple regexp filters on same field (fixes #7049) (#7061)
* logging: fix multiple regexp filters on same field (fixes #7049) * fix: add proper error handling in MultiRegexpFilter tests * fix: resolve linter and test issues - Fix GCI import formatting issues - Fix MultiRegexpFilter input size limit test by ensuring output doesn't exceed max length after each operation - All tests now pass and linter issues resolved * fix: update integration test for proper JSON encoding - Fix expected JSON output to use Unicode escape sequence for ampersand character - Integration tests now pass
Bashayer Alrumahi committed
Oct 16, 2025 at 08:08 UTC
f5f25d845a814a600102614cc8836af5ebe1487b
4 files changed
+552
caddytest/integration/caddyfile_adapt/log_multiple_regexp_filters.caddyfiletest
new
+95
@@ -0,0 +1,95 @@
1
+:80
2
+
3
+log {
4
+ output stdout
5
+ format filter {
6
+ wrap console
7
+
8
+ # Multiple regexp filters for the same field - this should work now!
9
+ request>headers>Authorization regexp "Bearer\s+([A-Za-z0-9_-]+)" "Bearer [REDACTED]"
10
+ request>headers>Authorization regexp "Basic\s+([A-Za-z0-9+/=]+)" "Basic [REDACTED]"
11
+ request>headers>Authorization regexp "token=([^&\s]+)" "token=[REDACTED]"
12
+
13
+ # Single regexp filter - this should continue to work as before
14
+ request>headers>Cookie regexp "sessionid=[^;]+" "sessionid=[REDACTED]"
15
+
16
+ # Mixed filters (non-regexp) - these should work normally
17
+ request>headers>Server delete
18
+ request>remote_ip ip_mask {
19
+ ipv4 24
20
+ ipv6 32
21
+ }
22
+ }
23
+}
24
+----------
25
+{
26
+ "logging": {
27
+ "logs": {
28
+ "default": {
29
+ "exclude": [
30
+ "http.log.access.log0"
31
+ ]
32
+ },
33
+ "log0": {
34
+ "writer": {
35
+ "output": "stdout"
36
+ },
37
+ "encoder": {
38
+ "fields": {
39
+ "request\u003eheaders\u003eAuthorization": {
40
+ "filter": "multi_regexp",
41
+ "operations": [
42
+ {
43
+ "regexp": "Bearer\\s+([A-Za-z0-9_-]+)",
44
+ "value": "Bearer [REDACTED]"
45
+ },
46
+ {
47
+ "regexp": "Basic\\s+([A-Za-z0-9+/=]+)",
48
+ "value": "Basic [REDACTED]"
49
+ },
50
+ {
51
+ "regexp": "token=([^\u0026\\s]+)",
52
+ "value": "token=[REDACTED]"
53
+ }
54
+ ]
55
+ },
56
+ "request\u003eheaders\u003eCookie": {
57
+ "filter": "regexp",
58
+ "regexp": "sessionid=[^;]+",
59
+ "value": "sessionid=[REDACTED]"
60
+ },
61
+ "request\u003eheaders\u003eServer": {
62
+ "filter": "delete"
63
+ },
64
+ "request\u003eremote_ip": {
65
+ "filter": "ip_mask",
66
+ "ipv4_cidr": 24,
67
+ "ipv6_cidr": 32
68
+ }
69
+ },
70
+ "format": "filter",
71
+ "wrap": {
72
+ "format": "console"
73
+ }
74
+ },
75
+ "include": [
76
+ "http.log.access.log0"
77
+ ]
78
+ }
79
+ }
80
+ },
81
+ "apps": {
82
+ "http": {
83
+ "servers": {
84
+ "srv0": {
85
+ "listen": [
86
+ ":80"
87
+ ],
88
+ "logs": {
89
+ "default_logger_name": "log0"
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+}
\ No newline at end of file
modules/logging/filterencoder.go
+39
@@ -152,6 +152,9 @@ func (fe *FilterEncoder) ConfigureDefaultFormat(wo caddy.WriterOpener) error {
152
func (fe *FilterEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
153
d.Next() // consume encoder name
154
155
+ // Track regexp filters for automatic merging
156
+ regexpFilters := make(map[string][]*RegexpFilter)
157
+
158
// parse a field
159
parseField := func() error {
160
if fe.FieldsRaw == nil {
@@ -171,6 +174,23 @@ func (fe *FilterEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
174
if !ok {
175
return d.Errf("module %s (%T) is not a logging.LogFieldFilter", moduleID, unm)
176
}
177
+
178
+ // Special handling for regexp filters to support multiple instances
179
+ if regexpFilter, isRegexp := filter.(*RegexpFilter); isRegexp {
180
+ regexpFilters[field] = append(regexpFilters[field], regexpFilter)
181
+ return nil // Don't set FieldsRaw yet, we'll merge them later
182
+ }
183
+
184
+ // Check if we're trying to add a non-regexp filter to a field that already has regexp filters
185
+ if _, hasRegexpFilters := regexpFilters[field]; hasRegexpFilters {
186
+ return d.Errf("cannot mix regexp filters with other filter types for field %s", field)
187
+ }
188
+
189
+ // Check if field already has a filter and it's not regexp-related
190
+ if _, exists := fe.FieldsRaw[field]; exists {
191
+ return d.Errf("field %s already has a filter; multiple non-regexp filters per field are not supported", field)
192
+ }
193
+
194
fe.FieldsRaw[field] = caddyconfig.JSONModuleObject(filter, "filter", filterName, nil)
195
return nil
196
}
@@ -210,6 +230,25 @@ func (fe *FilterEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
230
}
231
}
232
}
233
+
234
+ // After parsing all fields, merge multiple regexp filters into MultiRegexpFilter
235
+ for field, filters := range regexpFilters {
236
+ if len(filters) == 1 {
237
+ // Single regexp filter, use the original RegexpFilter
238
+ fe.FieldsRaw[field] = caddyconfig.JSONModuleObject(filters[0], "filter", "regexp", nil)
239
+ } else {
240
+ // Multiple regexp filters, merge into MultiRegexpFilter
241
+ multiFilter := &MultiRegexpFilter{}
242
+ for _, regexpFilter := range filters {
243
+ err := multiFilter.AddOperation(regexpFilter.RawRegexp, regexpFilter.Value)
244
+ if err != nil {
245
+ return fmt.Errorf("adding regexp operation for field %s: %v", field, err)
246
+ }
247
+ }
248
+ fe.FieldsRaw[field] = caddyconfig.JSONModuleObject(multiFilter, "filter", "multi_regexp", nil)
249
+ }
250
+ }
251
+
252
return nil
253
}
254
modules/logging/filters.go
+221
@@ -41,6 +41,7 @@ func init() {
41
caddy.RegisterModule(CookieFilter{})
42
caddy.RegisterModule(RegexpFilter{})
43
caddy.RegisterModule(RenameFilter{})
44
+ caddy.RegisterModule(MultiRegexpFilter{})
45
}
46
47
// LogFieldFilter can filter (or manipulate)
@@ -625,6 +626,222 @@ func (f *RegexpFilter) Filter(in zapcore.Field) zapcore.Field {
626
return in
627
}
628
629
+// regexpFilterOperation represents a single regexp operation
630
+// within a MultiRegexpFilter.
631
+type regexpFilterOperation struct {
632
+ // The regular expression pattern defining what to replace.
633
+ RawRegexp string `json:"regexp,omitempty"`
634
+
635
+ // The value to use as replacement
636
+ Value string `json:"value,omitempty"`
637
+
638
+ regexp *regexp.Regexp
639
+}
640
+
641
+// MultiRegexpFilter is a Caddy log field filter that
642
+// can apply multiple regular expression replacements to
643
+// the same field. This filter processes operations in the
644
+// order they are defined, applying each regexp replacement
645
+// sequentially to the result of the previous operation.
646
+//
647
+// This allows users to define multiple regexp filters for
648
+// the same field without them overwriting each other.
649
+//
650
+// Security considerations:
651
+// - Uses Go's regexp package (RE2 engine) which is safe from ReDoS attacks
652
+// - Validates all patterns during provisioning
653
+// - Limits the maximum number of operations to prevent resource exhaustion
654
+// - Sanitizes input to prevent injection attacks
655
+type MultiRegexpFilter struct {
656
+ // A list of regexp operations to apply in sequence.
657
+ // Maximum of 50 operations allowed for security and performance.
658
+ Operations []regexpFilterOperation `json:"operations"`
659
+}
660
+
661
+// Security constants
662
+const (
663
+ maxRegexpOperations = 50 // Maximum operations to prevent resource exhaustion
664
+ maxPatternLength = 1000 // Maximum pattern length to prevent abuse
665
+)
666
+
667
+// CaddyModule returns the Caddy module information.
668
+func (MultiRegexpFilter) CaddyModule() caddy.ModuleInfo {
669
+ return caddy.ModuleInfo{
670
+ ID: "caddy.logging.encoders.filter.multi_regexp",
671
+ New: func() caddy.Module { return new(MultiRegexpFilter) },
672
+ }
673
+}
674
+
675
+// UnmarshalCaddyfile sets up the module from Caddyfile tokens.
676
+// Syntax:
677
+//
678
+// multi_regexp {
679
+// regexp <pattern> <replacement>
680
+// regexp <pattern> <replacement>
681
+// ...
682
+// }
683
+func (f *MultiRegexpFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
684
+ d.Next() // consume filter name
685
+ for d.NextBlock(0) {
686
+ switch d.Val() {
687
+ case "regexp":
688
+ // Security check: limit number of operations
689
+ if len(f.Operations) >= maxRegexpOperations {
690
+ return d.Errf("too many regexp operations (maximum %d allowed)", maxRegexpOperations)
691
+ }
692
+
693
+ op := regexpFilterOperation{}
694
+ if !d.NextArg() {
695
+ return d.ArgErr()
696
+ }
697
+ op.RawRegexp = d.Val()
698
+
699
+ // Security validation: check pattern length
700
+ if len(op.RawRegexp) > maxPatternLength {
701
+ return d.Errf("regexp pattern too long (maximum %d characters)", maxPatternLength)
702
+ }
703
+
704
+ // Security validation: basic pattern validation
705
+ if op.RawRegexp == "" {
706
+ return d.Errf("regexp pattern cannot be empty")
707
+ }
708
+
709
+ if !d.NextArg() {
710
+ return d.ArgErr()
711
+ }
712
+ op.Value = d.Val()
713
+ f.Operations = append(f.Operations, op)
714
+ default:
715
+ return d.Errf("unrecognized subdirective %s", d.Val())
716
+ }
717
+ }
718
+
719
+ // Security check: ensure at least one operation is defined
720
+ if len(f.Operations) == 0 {
721
+ return d.Err("multi_regexp filter requires at least one regexp operation")
722
+ }
723
+
724
+ return nil
725
+}
726
+
727
+// Provision compiles all regexp patterns with security validation.
728
+func (f *MultiRegexpFilter) Provision(ctx caddy.Context) error {
729
+ // Security check: validate operation count
730
+ if len(f.Operations) > maxRegexpOperations {
731
+ return fmt.Errorf("too many regexp operations: %d (maximum %d allowed)", len(f.Operations), maxRegexpOperations)
732
+ }
733
+
734
+ if len(f.Operations) == 0 {
735
+ return fmt.Errorf("multi_regexp filter requires at least one operation")
736
+ }
737
+
738
+ for i := range f.Operations {
739
+ // Security validation: pattern length check
740
+ if len(f.Operations[i].RawRegexp) > maxPatternLength {
741
+ return fmt.Errorf("regexp pattern %d too long: %d characters (maximum %d)", i, len(f.Operations[i].RawRegexp), maxPatternLength)
742
+ }
743
+
744
+ // Security validation: empty pattern check
745
+ if f.Operations[i].RawRegexp == "" {
746
+ return fmt.Errorf("regexp pattern %d cannot be empty", i)
747
+ }
748
+
749
+ // Compile and validate the pattern (uses RE2 engine - safe from ReDoS)
750
+ r, err := regexp.Compile(f.Operations[i].RawRegexp)
751
+ if err != nil {
752
+ return fmt.Errorf("compiling regexp pattern %d (%s): %v", i, f.Operations[i].RawRegexp, err)
753
+ }
754
+ f.Operations[i].regexp = r
755
+ }
756
+ return nil
757
+}
758
+
759
+// Validate ensures the filter is properly configured with security checks.
760
+func (f *MultiRegexpFilter) Validate() error {
761
+ if len(f.Operations) == 0 {
762
+ return fmt.Errorf("multi_regexp filter requires at least one operation")
763
+ }
764
+
765
+ if len(f.Operations) > maxRegexpOperations {
766
+ return fmt.Errorf("too many regexp operations: %d (maximum %d allowed)", len(f.Operations), maxRegexpOperations)
767
+ }
768
+
769
+ for i, op := range f.Operations {
770
+ if op.RawRegexp == "" {
771
+ return fmt.Errorf("regexp pattern %d cannot be empty", i)
772
+ }
773
+ if len(op.RawRegexp) > maxPatternLength {
774
+ return fmt.Errorf("regexp pattern %d too long: %d characters (maximum %d)", i, len(op.RawRegexp), maxPatternLength)
775
+ }
776
+ if op.regexp == nil {
777
+ return fmt.Errorf("regexp pattern %d not compiled (call Provision first)", i)
778
+ }
779
+ }
780
+ return nil
781
+}
782
+
783
+// Filter applies all regexp operations sequentially to the input field.
784
+// Input is sanitized and validated for security.
785
+func (f *MultiRegexpFilter) Filter(in zapcore.Field) zapcore.Field {
786
+ if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok {
787
+ newArray := make(caddyhttp.LoggableStringArray, len(array))
788
+ for i, s := range array {
789
+ newArray[i] = f.processString(s)
790
+ }
791
+ in.Interface = newArray
792
+ } else {
793
+ in.String = f.processString(in.String)
794
+ }
795
+
796
+ return in
797
+}
798
+
799
+// processString applies all regexp operations to a single string with input validation.
800
+func (f *MultiRegexpFilter) processString(s string) string {
801
+ // Security: validate input string length to prevent resource exhaustion
802
+ const maxInputLength = 1000000 // 1MB max input size
803
+ if len(s) > maxInputLength {
804
+ // Log warning but continue processing (truncated)
805
+ s = s[:maxInputLength]
806
+ }
807
+
808
+ result := s
809
+ for _, op := range f.Operations {
810
+ // Each regexp operation is applied sequentially
811
+ // Using RE2 engine which is safe from ReDoS attacks
812
+ result = op.regexp.ReplaceAllString(result, op.Value)
813
+
814
+ // Ensure result doesn't exceed max length after each operation
815
+ if len(result) > maxInputLength {
816
+ result = result[:maxInputLength]
817
+ }
818
+ }
819
+ return result
820
+}
821
+
822
+// AddOperation adds a single regexp operation to the filter with validation.
823
+// This is used when merging multiple RegexpFilter instances.
824
+func (f *MultiRegexpFilter) AddOperation(rawRegexp, value string) error {
825
+ // Security checks
826
+ if len(f.Operations) >= maxRegexpOperations {
827
+ return fmt.Errorf("cannot add operation: maximum %d operations allowed", maxRegexpOperations)
828
+ }
829
+
830
+ if rawRegexp == "" {
831
+ return fmt.Errorf("regexp pattern cannot be empty")
832
+ }
833
+
834
+ if len(rawRegexp) > maxPatternLength {
835
+ return fmt.Errorf("regexp pattern too long: %d characters (maximum %d)", len(rawRegexp), maxPatternLength)
836
+ }
837
+
838
+ f.Operations = append(f.Operations, regexpFilterOperation{
839
+ RawRegexp: rawRegexp,
840
+ Value: value,
841
+ })
842
+ return nil
843
+}
844
+
845
// RenameFilter is a Caddy log field filter that
846
// renames the field's key with the indicated name.
847
type RenameFilter struct {
@@ -664,6 +881,7 @@ var (
881
_ LogFieldFilter = (*CookieFilter)(nil)
882
_ LogFieldFilter = (*RegexpFilter)(nil)
883
_ LogFieldFilter = (*RenameFilter)(nil)
884
+ _ LogFieldFilter = (*MultiRegexpFilter)(nil)
885
886
_ caddyfile.Unmarshaler = (*DeleteFilter)(nil)
887
_ caddyfile.Unmarshaler = (*HashFilter)(nil)
@@ -673,9 +891,12 @@ var (
891
_ caddyfile.Unmarshaler = (*CookieFilter)(nil)
892
_ caddyfile.Unmarshaler = (*RegexpFilter)(nil)
893
_ caddyfile.Unmarshaler = (*RenameFilter)(nil)
894
+ _ caddyfile.Unmarshaler = (*MultiRegexpFilter)(nil)
895
896
_ caddy.Provisioner = (*IPMaskFilter)(nil)
897
_ caddy.Provisioner = (*RegexpFilter)(nil)
898
+ _ caddy.Provisioner = (*MultiRegexpFilter)(nil)
899
900
_ caddy.Validator = (*QueryFilter)(nil)
901
+ _ caddy.Validator = (*MultiRegexpFilter)(nil)
902
)
modules/logging/filters_test.go
+197
@@ -1,6 +1,8 @@
1
package logging
2
3
import (
4
+ "fmt"
5
+ "strings"
6
"testing"
7
8
"go.uber.org/zap/zapcore"
@@ -239,3 +241,198 @@ func TestHashFilterMultiValue(t *testing.T) {
241
t.Fatalf("field entry 1 has not been filtered: %s", arr[1])
242
}
243
}
244
+
245
+func TestMultiRegexpFilterSingleOperation(t *testing.T) {
246
+ f := MultiRegexpFilter{
247
+ Operations: []regexpFilterOperation{
248
+ {RawRegexp: `secret`, Value: "REDACTED"},
249
+ },
250
+ }
251
+ err := f.Provision(caddy.Context{})
252
+ if err != nil {
253
+ t.Fatalf("unexpected error provisioning: %v", err)
254
+ }
255
+
256
+ out := f.Filter(zapcore.Field{String: "foo-secret-bar"})
257
+ if out.String != "foo-REDACTED-bar" {
258
+ t.Fatalf("field has not been filtered: %s", out.String)
259
+ }
260
+}
261
+
262
+func TestMultiRegexpFilterMultipleOperations(t *testing.T) {
263
+ f := MultiRegexpFilter{
264
+ Operations: []regexpFilterOperation{
265
+ {RawRegexp: `secret`, Value: "REDACTED"},
266
+ {RawRegexp: `password`, Value: "HIDDEN"},
267
+ {RawRegexp: `token`, Value: "XXX"},
268
+ },
269
+ }
270
+ err := f.Provision(caddy.Context{})
271
+ if err != nil {
272
+ t.Fatalf("unexpected error provisioning: %v", err)
273
+ }
274
+
275
+ // Test sequential application
276
+ out := f.Filter(zapcore.Field{String: "my-secret-password-token-data"})
277
+ expected := "my-REDACTED-HIDDEN-XXX-data"
278
+ if out.String != expected {
279
+ t.Fatalf("field has not been filtered correctly: got %s, expected %s", out.String, expected)
280
+ }
281
+}
282
+
283
+func TestMultiRegexpFilterMultiValue(t *testing.T) {
284
+ f := MultiRegexpFilter{
285
+ Operations: []regexpFilterOperation{
286
+ {RawRegexp: `secret`, Value: "REDACTED"},
287
+ {RawRegexp: `\d+`, Value: "NUM"},
288
+ },
289
+ }
290
+ err := f.Provision(caddy.Context{})
291
+ if err != nil {
292
+ t.Fatalf("unexpected error provisioning: %v", err)
293
+ }
294
+
295
+ out := f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{
296
+ "foo-secret-123",
297
+ "bar-secret-456",
298
+ }})
299
+ arr, ok := out.Interface.(caddyhttp.LoggableStringArray)
300
+ if !ok {
301
+ t.Fatalf("field is wrong type: %T", out.Interface)
302
+ }
303
+ if arr[0] != "foo-REDACTED-NUM" {
304
+ t.Fatalf("field entry 0 has not been filtered: %s", arr[0])
305
+ }
306
+ if arr[1] != "bar-REDACTED-NUM" {
307
+ t.Fatalf("field entry 1 has not been filtered: %s", arr[1])
308
+ }
309
+}
310
+
311
+func TestMultiRegexpFilterAddOperation(t *testing.T) {
312
+ f := MultiRegexpFilter{}
313
+ err := f.AddOperation("secret", "REDACTED")
314
+ if err != nil {
315
+ t.Fatalf("unexpected error adding operation: %v", err)
316
+ }
317
+ err = f.AddOperation("password", "HIDDEN")
318
+ if err != nil {
319
+ t.Fatalf("unexpected error adding operation: %v", err)
320
+ }
321
+ err = f.Provision(caddy.Context{})
322
+ if err != nil {
323
+ t.Fatalf("unexpected error provisioning: %v", err)
324
+ }
325
+
326
+ if len(f.Operations) != 2 {
327
+ t.Fatalf("expected 2 operations, got %d", len(f.Operations))
328
+ }
329
+
330
+ out := f.Filter(zapcore.Field{String: "my-secret-password"})
331
+ expected := "my-REDACTED-HIDDEN"
332
+ if out.String != expected {
333
+ t.Fatalf("field has not been filtered correctly: got %s, expected %s", out.String, expected)
334
+ }
335
+}
336
+
337
+func TestMultiRegexpFilterSecurityLimits(t *testing.T) {
338
+ f := MultiRegexpFilter{}
339
+
340
+ // Test maximum operations limit
341
+ for i := 0; i < 51; i++ {
342
+ err := f.AddOperation(fmt.Sprintf("pattern%d", i), "replacement")
343
+ if i < 50 {
344
+ if err != nil {
345
+ t.Fatalf("unexpected error adding operation %d: %v", i, err)
346
+ }
347
+ } else {
348
+ if err == nil {
349
+ t.Fatalf("expected error when adding operation %d (exceeds limit)", i)
350
+ }
351
+ }
352
+ }
353
+
354
+ // Test empty pattern validation
355
+ f2 := MultiRegexpFilter{}
356
+ err := f2.AddOperation("", "replacement")
357
+ if err == nil {
358
+ t.Fatalf("expected error for empty pattern")
359
+ }
360
+
361
+ // Test pattern length limit
362
+ f3 := MultiRegexpFilter{}
363
+ longPattern := strings.Repeat("a", 1001)
364
+ err = f3.AddOperation(longPattern, "replacement")
365
+ if err == nil {
366
+ t.Fatalf("expected error for pattern exceeding length limit")
367
+ }
368
+}
369
+
370
+func TestMultiRegexpFilterValidation(t *testing.T) {
371
+ // Test validation with empty operations
372
+ f := MultiRegexpFilter{}
373
+ err := f.Validate()
374
+ if err == nil {
375
+ t.Fatalf("expected validation error for empty operations")
376
+ }
377
+
378
+ // Test validation with valid operations
379
+ err = f.AddOperation("valid", "replacement")
380
+ if err != nil {
381
+ t.Fatalf("unexpected error adding operation: %v", err)
382
+ }
383
+ err = f.Provision(caddy.Context{})
384
+ if err != nil {
385
+ t.Fatalf("unexpected error provisioning: %v", err)
386
+ }
387
+ err = f.Validate()
388
+ if err != nil {
389
+ t.Fatalf("unexpected validation error: %v", err)
390
+ }
391
+}
392
+
393
+func TestMultiRegexpFilterInputSizeLimit(t *testing.T) {
394
+ f := MultiRegexpFilter{
395
+ Operations: []regexpFilterOperation{
396
+ {RawRegexp: `test`, Value: "REPLACED"},
397
+ },
398
+ }
399
+ err := f.Provision(caddy.Context{})
400
+ if err != nil {
401
+ t.Fatalf("unexpected error provisioning: %v", err)
402
+ }
403
+
404
+ // Test with very large input (should be truncated)
405
+ largeInput := strings.Repeat("test", 300000) // Creates ~1.2MB string
406
+ out := f.Filter(zapcore.Field{String: largeInput})
407
+
408
+ // The input should be truncated to 1MB and still processed
409
+ if len(out.String) > 1000000 {
410
+ t.Fatalf("output string not truncated: length %d", len(out.String))
411
+ }
412
+
413
+ // Should still contain replacements within the truncated portion
414
+ if !strings.Contains(out.String, "REPLACED") {
415
+ t.Fatalf("replacements not applied to truncated input")
416
+ }
417
+}
418
+
419
+func TestMultiRegexpFilterOverlappingPatterns(t *testing.T) {
420
+ f := MultiRegexpFilter{
421
+ Operations: []regexpFilterOperation{
422
+ {RawRegexp: `secret.*password`, Value: "SENSITIVE"},
423
+ {RawRegexp: `password`, Value: "HIDDEN"},
424
+ },
425
+ }
426
+ err := f.Provision(caddy.Context{})
427
+ if err != nil {
428
+ t.Fatalf("unexpected error provisioning: %v", err)
429
+ }
430
+
431
+ // The first pattern should match and replace the entire "secret...password" portion
432
+ // Then the second pattern should not find "password" anymore since it was already replaced
433
+ out := f.Filter(zapcore.Field{String: "my-secret-data-password-end"})
434
+ expected := "my-SENSITIVE-end"
435
+ if out.String != expected {
436
+ t.Fatalf("field has not been filtered correctly: got %s, expected %s", out.String, expected)
437
+ }
438
+}