From 5a18b84ae3fd6b2ecfea6cad185276e226e3da6f Mon Sep 17 00:00:00 2001 From: Evan Vetere Date: Fri, 10 Jul 2026 15:33:22 -0400 Subject: [PATCH 1/2] fix: clone RouteBaseDirectives before per-policy append getCorazaDirectivesForTrafficProtectionPolicy seeded its per-policy directive list by aliasing the operator's shared RouteBaseDirectives slice, then appended to it. When the shared slice had spare capacity (cap > len), the append wrote into the shared backing array, leaking per-policy directives (SecRuleEngine mode, score thresholds, rule removals) into the base seen by other policies. Clone the base slice before appending, matching the safe precedent in internal/extensionserver/cache/index.go. Latent today: the builder is reached only under IsEPPEmissionEnabled(), off by default. Fixes #246 --- internal/controller/trafficprotectionpolicy_controller.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/controller/trafficprotectionpolicy_controller.go b/internal/controller/trafficprotectionpolicy_controller.go index 1bfb4587..79c5db58 100644 --- a/internal/controller/trafficprotectionpolicy_controller.go +++ b/internal/controller/trafficprotectionpolicy_controller.go @@ -6,6 +6,7 @@ import ( "context" "encoding/json" "fmt" + "slices" "sort" "strings" @@ -1229,7 +1230,7 @@ func (r *TrafficProtectionPolicyReconciler) getCorazaDirectivesForTrafficProtect secRuleEngine = "Off" } - directives := r.Config.Gateway.Coraza.RouteBaseDirectives + directives := slices.Clone(r.Config.Gateway.Coraza.RouteBaseDirectives) directives = append(directives, fmt.Sprintf("SecRuleEngine %s", secRuleEngine)) From 5b2cc69ce25a4020e03842b6531e54090171e972 Mon Sep 17 00:00:00 2001 From: Evan Vetere Date: Fri, 10 Jul 2026 15:35:16 -0400 Subject: [PATCH 2/2] test: guard against RouteBaseDirectives aliasing leak Add TestGetCorazaDirectivesDoesNotAliasRouteBaseDirectives: seed the shared base slice with spare capacity, build an Enforce policy, then build a second Observe policy and assert the first's SecRuleEngine token was not overwritten. Fails on the aliased code, passes with the slices.Clone fix. --- ...trafficprotectionpolicy_controller_test.go | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/internal/controller/trafficprotectionpolicy_controller_test.go b/internal/controller/trafficprotectionpolicy_controller_test.go index 1fd6989b..5b9c9e49 100644 --- a/internal/controller/trafficprotectionpolicy_controller_test.go +++ b/internal/controller/trafficprotectionpolicy_controller_test.go @@ -992,6 +992,36 @@ func TestGetCorazaDirectivesForTrafficProtectionPolicy(t *testing.T) { } } +func TestGetCorazaDirectivesDoesNotAliasRouteBaseDirectives(t *testing.T) { + base := make([]string, 2, 8) + base[0] = "Include @crs-setup-conf" + base[1] = "Include @recommended-conf" + + operatorConfig := config.NetworkServicesOperator{ + Gateway: config.GatewayConfig{ + Coraza: config.CorazaConfig{RouteBaseDirectives: base}, + }, + } + reconciler := &TrafficProtectionPolicyReconciler{Config: operatorConfig} + + enforce := reconciler.getCorazaDirectivesForTrafficProtectionPolicy(&policyContext{ptr.To( + newTrafficProtectionPolicy("default", "tpp-enforce", func(tpp *networkingv1alpha.TrafficProtectionPolicy) { + tpp.Spec.Mode = networkingv1alpha.TrafficProtectionPolicyEnforce + }), + )}) + assert.Contains(t, enforce, "SecRuleEngine On") + + reconciler.getCorazaDirectivesForTrafficProtectionPolicy(&policyContext{ptr.To( + newTrafficProtectionPolicy("default", "tpp-observe"), + )}) + + assert.Contains(t, enforce, "SecRuleEngine On", + "second policy leaked into the first via a shared RouteBaseDirectives backing array") + assert.EqualValues(t, []string{"Include @crs-setup-conf", "Include @recommended-conf"}, + reconciler.Config.Gateway.Coraza.RouteBaseDirectives, + "shared base directives were mutated") +} + // nolint:unparam func newTrafficProtectionPolicy( namespace,