CVE-2026-39961
Description
Aiven Operator allows you to provision and manage Aiven Services from your Kubernetes cluster. From 0.31.0 to before 0.37.0, a developer with create permission on ClickhouseUser CRDs in their own namespace can exfiltrate secrets from any other namespace — production database credentials, API keys, service tokens — with a single kubectl apply. The operator reads the victim's secret using its ClusterRole and writes the password into a new secret in the attacker's namespace. The operator acts as a confused deputy: its ServiceAccount has cluster-wide secret read/write (aiven-operator-role ClusterRole), and it trusts user-supplied namespace values in spec.connInfoSecretSource.namespace without validation. No admission webhook enforces this boundary — the ServiceUser webhook returns nil, and no ClickhouseUser webhook exists. This vulnerability is fixed in 0.37.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/aiven/aiven-operatorGo | >= 0.31.0, < 0.37.0 | 0.37.0 |
Affected products
2(expand)+ 1 more
- (no CPE)
- cpe:2.3:a:aiven:aiven_operator:*:*:*:*:*:*:*:*range: >=0.31.0,<0.37.0
Patches
1032c9ba63257Merge commit from fork
16 files changed · +47 −393
api/v1alpha1/common.go+2 −3 modified@@ -46,10 +46,9 @@ type ConnInfoSecretTarget struct { type ConnInfoSecretSource struct { // +kubebuilder:validation:Required // +kubebuilder:validation:MinLength=1 - // Name of the secret resource to read connection parameters from + // Name of the secret resource to read connection parameters from. + // The secret must be in the same namespace as the resource. Name string `json:"name"` - // Namespace of the source secret. If not specified, defaults to the same namespace as the resource - Namespace string `json:"namespace,omitempty"` // +kubebuilder:validation:Required // +kubebuilder:validation:MinLength=1 // Key in the secret containing the password to use for authentication
CHANGELOG.md+3 −0 modified@@ -5,6 +5,9 @@ - Add `ServiceUser` field `accessControl`, type `object`: AccessControl configures service-specific access control rules for the user. When this block is present, the operator manages the full access-control scope it contains - Add `OpenSearchACLConfig` to manage OpenSearch ACL +- **BREAKING**: Removed `ClickhouseUser`/`ServiceUser` field `connInfoSecretSource.namespace`, type `string`: cross-namespace + secret references are no longer supported. The source secret must be in the same namespace as the resource. + This fixes a potential confused deputy vulnerability where the operator could be exploited to exfiltrate secrets from other namespaces. ## v0.36.0 - 2026-03-05
charts/aiven-operator-crds/templates/aiven.io_clickhouseusers.yaml+3 −8 modified@@ -77,16 +77,11 @@ spec: when the secret data is updated. properties: name: - description: - Name of the secret resource to read connection parameters - from + description: |- + Name of the secret resource to read connection parameters from. + The secret must be in the same namespace as the resource. minLength: 1 type: string - namespace: - description: - Namespace of the source secret. If not specified, - defaults to the same namespace as the resource - type: string passwordKey: description: Key in the secret containing the password to use
charts/aiven-operator-crds/templates/aiven.io_serviceusers.yaml+3 −8 modified@@ -110,16 +110,11 @@ spec: when the secret data is updated. properties: name: - description: - Name of the secret resource to read connection parameters - from + description: |- + Name of the secret resource to read connection parameters from. + The secret must be in the same namespace as the resource. minLength: 1 type: string - namespace: - description: - Namespace of the source secret. If not specified, - defaults to the same namespace as the resource - type: string passwordKey: description: Key in the secret containing the password to use
config/crd/bases/aiven.io_clickhouseusers.yaml+3 −8 modified@@ -77,16 +77,11 @@ spec: when the secret data is updated. properties: name: - description: - Name of the secret resource to read connection parameters - from + description: |- + Name of the secret resource to read connection parameters from. + The secret must be in the same namespace as the resource. minLength: 1 type: string - namespace: - description: - Namespace of the source secret. If not specified, - defaults to the same namespace as the resource - type: string passwordKey: description: Key in the secret containing the password to use
config/crd/bases/aiven.io_serviceusers.yaml+3 −8 modified@@ -110,16 +110,11 @@ spec: when the secret data is updated. properties: name: - description: - Name of the secret resource to read connection parameters - from + description: |- + Name of the secret resource to read connection parameters from. + The secret must be in the same namespace as the resource. minLength: 1 type: string - namespace: - description: - Namespace of the source secret. If not specified, - defaults to the same namespace as the resource - type: string passwordKey: description: Key in the secret containing the password to use
controllers/secret_password_manager.go+5 −8 modified@@ -25,31 +25,28 @@ func GetPasswordFromSecret(ctx context.Context, k8sClient client.Client, resourc return "", nil } - sourceNamespace := secretSource.Namespace - if sourceNamespace == "" { - sourceNamespace = resource.GetNamespace() - } + ns := resource.GetNamespace() sourceSecret := &corev1.Secret{} err := k8sClient.Get(ctx, types.NamespacedName{ Name: secretSource.Name, - Namespace: sourceNamespace, + Namespace: ns, }, sourceSecret) if err != nil { - return "", fmt.Errorf("failed to read connInfoSecretSource %s/%s: %w", sourceNamespace, secretSource.Name, err) + return "", fmt.Errorf("failed to read connInfoSecretSource %s/%s: %w", ns, secretSource.Name, err) } passwordBytes, exists := sourceSecret.Data[secretSource.PasswordKey] if !exists { - return "", fmt.Errorf("password not found in source secret %s/%s (expected %s key)", sourceNamespace, secretSource.Name, secretSource.PasswordKey) + return "", fmt.Errorf("password not found in source secret %s/%s (expected %s key)", ns, secretSource.Name, secretSource.PasswordKey) } newPassword := string(passwordBytes) // validate password length according to API requirements if len(newPassword) < 8 || len(newPassword) > 256 { return "", fmt.Errorf("password length must be between 8 and 256 characters, got %d characters from source secret %s/%s (key: %s)", - len(newPassword), sourceNamespace, secretSource.Name, secretSource.PasswordKey) + len(newPassword), ns, secretSource.Name, secretSource.PasswordKey) } return newPassword, nil
controllers/secret_password_manager_test.go+21 −80 modified@@ -53,26 +53,6 @@ func TestPasswordManager_GetPasswordFromSecret(t *testing.T) { expectedResult: "ValidPassword123!", expectError: false, }, - { - name: "Valid password from secret in different namespace", - secretSource: &v1alpha1.ConnInfoSecretSource{ - Name: "test-secret", - Namespace: "other-ns", - PasswordKey: "PASSWORD", - }, - resourceNS: "default", - secret: &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-secret", - Namespace: "other-ns", - }, - Data: map[string][]byte{ - "PASSWORD": []byte("CrossNSPassword123!"), - }, - }, - expectedResult: "CrossNSPassword123!", - expectError: false, - }, { name: "Secret not found", secretSource: &v1alpha1.ConnInfoSecretSource{ @@ -256,72 +236,33 @@ func TestPasswordManager_NamespaceResolution(t *testing.T) { require.NoError(t, corev1.AddToScheme(scheme)) require.NoError(t, v1alpha1.AddToScheme(scheme)) - tests := []struct { - name string - resourceNS string - sourceNS string - expectedNS string - secretExists bool - }{ - { - name: "Uses resource namespace when source namespace is empty", - resourceNS: "resource-ns", - sourceNS: "", - expectedNS: "resource-ns", - secretExists: true, + // Secret must always be in the same namespace as the resource + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: "resource-ns", }, - { - name: "Uses source namespace when specified", - resourceNS: "resource-ns", - sourceNS: "source-ns", - expectedNS: "source-ns", - secretExists: true, + Data: map[string][]byte{ + "PASSWORD": []byte("ValidPassword123!"), }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var objects []client.Object - if tt.secretExists { - secret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-secret", - Namespace: tt.expectedNS, - }, - Data: map[string][]byte{ - "PASSWORD": []byte("ValidPassword123!"), - }, - } - objects = append(objects, secret) - } - - secretSource := &v1alpha1.ConnInfoSecretSource{ + user := &v1alpha1.ClickhouseUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-user", + Namespace: "resource-ns", + }, + Spec: v1alpha1.ClickhouseUserSpec{ + ConnInfoSecretSource: &v1alpha1.ConnInfoSecretSource{ Name: "test-secret", PasswordKey: "PASSWORD", - } - if tt.sourceNS != "" { - secretSource.Namespace = tt.sourceNS - } - - user := &v1alpha1.ClickhouseUser{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-user", - Namespace: tt.resourceNS, - }, - Spec: v1alpha1.ClickhouseUserSpec{ - ConnInfoSecretSource: secretSource, - }, - } + }, + }, + } - k8sClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() - result, err := GetPasswordFromSecret(context.Background(), k8sClient, user) + k8sClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(secret).Build() + result, err := GetPasswordFromSecret(context.Background(), k8sClient, user) - if tt.secretExists { - require.NoError(t, err) - assert.Equal(t, "ValidPassword123!", result) - } else { - require.Error(t, err) - } - }) - } + require.NoError(t, err) + assert.Equal(t, "ValidPassword123!", result) }
controllers/secret_watch_controller.go+1 −6 modified@@ -112,12 +112,7 @@ func (c *SecretWatchController) getResourcesWithSecretSource() []SecretSourceRes func connInfoSecretRefIndexFunc(o client.Object) []string { if resource, ok := o.(SecretSourceResource); ok { if secretSource := resource.GetConnInfoSecretSource(); secretSource != nil { - sourceNamespace := secretSource.Namespace - if sourceNamespace == "" { - sourceNamespace = resource.GetNamespace() - } - - return []string{fmt.Sprintf("%s/%s", sourceNamespace, secretSource.Name)} + return []string{fmt.Sprintf("%s/%s", resource.GetNamespace(), secretSource.Name)} } }
controllers/secret_watch_controller_test.go+1 −63 modified@@ -132,23 +132,6 @@ func TestConnInfoSecretRefIndexFunc(t *testing.T) { }, expected: []string{"default/my-secret"}, }, - { - name: "ServiceUser with secretSource in different namespace", - resource: &v1alpha1.ServiceUser{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-user", - Namespace: "default", - }, - Spec: v1alpha1.ServiceUserSpec{ - ConnInfoSecretSource: &v1alpha1.ConnInfoSecretSource{ - Name: "my-secret", - Namespace: "other-namespace", - PasswordKey: "password", - }, - }, - }, - expected: []string{"other-namespace/my-secret"}, - }, { name: "ServiceUser without secretSource", resource: &v1alpha1.ServiceUser{ @@ -178,23 +161,6 @@ func TestConnInfoSecretRefIndexFunc(t *testing.T) { }, expected: []string{"default/ch-secret"}, }, - { - name: "ClickhouseUser with cross-namespace secretSource", - resource: &v1alpha1.ClickhouseUser{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-ch-user", - Namespace: "app-namespace", - }, - Spec: v1alpha1.ClickhouseUserSpec{ - ConnInfoSecretSource: &v1alpha1.ConnInfoSecretSource{ - Name: "shared-secret", - Namespace: "secrets-namespace", - PasswordKey: "password", - }, - }, - }, - expected: []string{"secrets-namespace/shared-secret"}, - }, { name: "Non-SecretSourceResource", resource: &corev1.Secret{ @@ -246,29 +212,6 @@ func TestSecretWatchController_resourceMatchesSecret(t *testing.T) { }, expected: true, }, - { - name: "ServiceUser matches secret in different namespace", - resource: &v1alpha1.ServiceUser{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-user", - Namespace: "app-ns", - }, - Spec: v1alpha1.ServiceUserSpec{ - ConnInfoSecretSource: &v1alpha1.ConnInfoSecretSource{ - Name: "my-secret", - Namespace: "secret-ns", - PasswordKey: "password", - }, - }, - }, - secret: &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "my-secret", - Namespace: "secret-ns", - }, - }, - expected: true, - }, { name: "ServiceUser does not match different secret name", resource: &v1alpha1.ServiceUser{ @@ -342,12 +285,7 @@ func TestSecretWatchController_resourceMatchesSecret(t *testing.T) { return } - sourceNamespace := secretSource.Namespace - if sourceNamespace == "" { - sourceNamespace = tt.resource.GetNamespace() - } - - matches := secretSource.Name == tt.secret.Name && sourceNamespace == tt.secret.Namespace + matches := secretSource.Name == tt.secret.Name && tt.resource.GetNamespace() == tt.secret.Namespace assert.Equal(t, tt.expected, matches) }) }
docs/docs/resources/clickhouseuser.md+1 −5 modified@@ -62,7 +62,6 @@ This resource uses the following API operations, and for each operation, _any_ o # Use existing secret for credential management connInfoSecretSource: name: predefined-credentials - # namespace: my-namespace # Optional: defaults to same namespace as ClickhouseUser passwordKey: PASSWORD project: aiven-project-name @@ -205,12 +204,9 @@ when the secret data is updated. **Required** - [`name`](#spec.connInfoSecretSource.name-property){: name='spec.connInfoSecretSource.name-property'} (string, MinLength: 1). Name of the secret resource to read connection parameters from. + The secret must be in the same namespace as the resource. - [`passwordKey`](#spec.connInfoSecretSource.passwordKey-property){: name='spec.connInfoSecretSource.passwordKey-property'} (string, MinLength: 1). Key in the secret containing the password to use for authentication. -**Optional** - -- [`namespace`](#spec.connInfoSecretSource.namespace-property){: name='spec.connInfoSecretSource.namespace-property'} (string). Namespace of the source secret. If not specified, defaults to the same namespace as the resource. - ## connInfoSecretTarget {: #spec.connInfoSecretTarget } _Appears on [`spec`](#spec)._
docs/docs/resources/examples/clickhouseuser.custom_credentials.yaml+0 −1 modified@@ -32,7 +32,6 @@ spec: # Use existing secret for credential management connInfoSecretSource: name: predefined-credentials - # namespace: my-namespace # Optional: defaults to same namespace as ClickhouseUser passwordKey: PASSWORD project: aiven-project-name
docs/docs/resources/examples/serviceuser.custom_credentials.yaml+0 −1 modified@@ -55,7 +55,6 @@ spec: # Use existing secret for credential management connInfoSecretSource: name: predefined-credentials - # namespace: my-namespace # Optional: defaults to same namespace as ServiceUser passwordKey: PASSWORD project: aiven-project-name
docs/docs/resources/serviceuser.md+1 −5 modified@@ -86,7 +86,6 @@ This resource uses the following API operations, and for each operation, _any_ o # Use existing secret for credential management connInfoSecretSource: name: predefined-credentials - # namespace: my-namespace # Optional: defaults to same namespace as ServiceUser passwordKey: PASSWORD project: aiven-project-name @@ -251,12 +250,9 @@ when the secret data is updated. **Required** - [`name`](#spec.connInfoSecretSource.name-property){: name='spec.connInfoSecretSource.name-property'} (string, MinLength: 1). Name of the secret resource to read connection parameters from. + The secret must be in the same namespace as the resource. - [`passwordKey`](#spec.connInfoSecretSource.passwordKey-property){: name='spec.connInfoSecretSource.passwordKey-property'} (string, MinLength: 1). Key in the secret containing the password to use for authentication. -**Optional** - -- [`namespace`](#spec.connInfoSecretSource.namespace-property){: name='spec.connInfoSecretSource.namespace-property'} (string). Namespace of the source secret. If not specified, defaults to the same namespace as the resource. - ## connInfoSecretTarget {: #spec.connInfoSecretTarget } _Appears on [`spec`](#spec)._
tests/clickhouseuser_test.go+0 −83 modified@@ -443,52 +443,6 @@ func TestClickhouseUserCustomCredentials(t *testing.T) { })) }) - t.Run("CrossNamespacePasswordSource", func(t *testing.T) { - // tests ClickhouseUser creation with a password from a different namespace - userName := randName("chu-cross-ns") - yml := getClickhouseUserWithCrossNamespaceSecretYaml(cfg.Project, chName, userName, cfg.PrimaryCloudName) - - require.NoError(t, s.Apply(yml)) - - user := new(v1alpha1.ClickhouseUser) - require.NoError(t, s.GetRunning(user, userName)) - - userAvn, err := getClickHouseUserByID(ctx, avnGen, cfg.Project, chName, user.Status.UUID) - require.NoError(t, err) - assert.Equal(t, userName, user.GetName()) - assert.Equal(t, userName, userAvn.Name) - assert.Equal(t, chName, user.Spec.ServiceName) - - secretName := fmt.Sprintf("my-clickhouse-user-secret-%s", userName) - secret, err := s.GetSecret(secretName) - require.NoError(t, err) - assert.NotEmpty(t, secret.Data["HOST"]) - assert.NotEmpty(t, secret.Data["PORT"]) - assert.NotEmpty(t, secret.Data["USERNAME"]) - assert.NotEmpty(t, secret.Data["PASSWORD"]) - - // verify the password matches cross-namespace secret value - actualPassword := string(secret.Data["PASSWORD"]) - assert.Equal(t, "CrossNamespacePassword456!", actualPassword, "Password should match cross-namespace secret value") - - assert.Equal(t, map[string]string{"test": "cross-namespace-password"}, secret.Annotations) - assert.Equal(t, map[string]string{"type": "cross-namespace"}, secret.Labels) - - // test ClickHouse connection with cross-namespace password - assert.NoError(t, pingClickhouse( - ctx, - secret.Data["CLICKHOUSEUSER_HOST"], - secret.Data["CLICKHOUSEUSER_PORT"], - secret.Data["CLICKHOUSEUSER_USERNAME"], - secret.Data["CLICKHOUSEUSER_PASSWORD"], - )) - - assert.NoError(t, s.Delete(user, func() error { - _, err = getClickHouseUserByID(ctx, avnGen, cfg.Project, chName, user.Status.UUID) - return err - })) - }) - t.Run("InvalidPasswordValidation", func(t *testing.T) { // tests validation of invalid passwords in connInfoSecretSource userName := randName("chu-invalid-pass") @@ -746,43 +700,6 @@ spec: `, project, chName, userName, cloudName) } -func getClickhouseUserWithCrossNamespaceSecretYaml(project, chName, userName, cloudName string) string { - return fmt.Sprintf(` -apiVersion: v1 -kind: Secret -metadata: - name: cross-namespace-password-secret-%[3]s - namespace: kube-system -data: - PASSWORD: Q3Jvc3NOYW1lc3BhY2VQYXNzd29yZDQ1NiE= # CrossNamespacePassword456! base64 encoded # gitleaks:allow ---- - -apiVersion: aiven.io/v1alpha1 -kind: ClickhouseUser -metadata: - name: %[3]s -spec: - authSecretRef: - name: aiven-token - key: token - - connInfoSecretTarget: - name: my-clickhouse-user-secret-%[3]s - annotations: - test: cross-namespace-password - labels: - type: cross-namespace - - connInfoSecretSource: - name: cross-namespace-password-secret-%[3]s - namespace: kube-system - passwordKey: PASSWORD - - project: %[1]s - serviceName: %[2]s -`, project, chName, userName, cloudName) -} - func getClickhouseUserWithInvalidPasswordYaml(project, chName, userName, cloudName string) string { return fmt.Sprintf(` apiVersion: v1
tests/serviceuser_watcher_test.go+0 −106 modified@@ -3,15 +3,13 @@ package tests import ( - "context" "fmt" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "github.com/aiven/aiven-operator/api/v1alpha1" @@ -211,110 +209,6 @@ spec: `, project, serviceName, userName, secretName, targetSecretName) } -// TestCrossNamespaceSecretWatch tests secret watching across namespaces -func TestCrossNamespaceSecretWatch(t *testing.T) { - defer recoverPanic(t) - - // GIVEN - ctx, cancel := testCtx() - defer cancel() - - pg, release, err := sharedResources.AcquirePostgreSQL(ctx) - require.NoError(t, err) - defer release() - - serviceName := pg.GetName() - userName := randName("cross-ns-user") - secretName := randName("cross-ns-secret") - secretNamespace := "test-secrets" - - testNs := &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{Name: secretNamespace}, - } - err = k8sClient.Create(ctx, testNs) - require.NoError(t, err) - - defer func() { - deleteCtx, deleteCancel := context.WithTimeout(context.Background(), 30*time.Second) - defer deleteCancel() - _ = k8sClient.Delete(deleteCtx, testNs) - }() - - yml := fmt.Sprintf(` -apiVersion: v1 -kind: Secret -metadata: - name: %[4]s - namespace: %[5]s -data: - password: Y3Jvc3MtbnMtcGFzc3dvcmQtMTIzNDU= # cross-ns-password-12345 # gitleaks:allow ---- -apiVersion: aiven.io/v1alpha1 -kind: ServiceUser -metadata: - name: %[3]s -spec: - authSecretRef: - name: aiven-token - key: token - - connInfoSecretTarget: - name: cross-ns-service-user-secret - - connInfoSecretSource: - name: %[4]s - namespace: %[5]s - passwordKey: password - - project: %[1]s - serviceName: %[2]s -`, cfg.Project, serviceName, userName, secretName, secretNamespace) - - s := NewSession(ctx, k8sClient) - defer s.Destroy(t) - - // WHEN - require.NoError(t, s.Apply(yml)) - - // wait for ServiceUser to be running - user := new(v1alpha1.ServiceUser) - require.NoError(t, s.GetRunning(user, userName)) - - // THEN - userAvn, err := avnGen.ServiceUserGet(ctx, cfg.Project, serviceName, userName) - require.NoError(t, err) - assert.Equal(t, userName, userAvn.Username) - - initialPassword := userAvn.Password - - secret := &corev1.Secret{} - err = k8sClient.Get(ctx, types.NamespacedName{ - Name: secretName, - Namespace: secretNamespace, - }, secret) - require.NoError(t, err) - - secret.Data["password"] = []byte("updated-cross-ns-password-67890") - err = k8sClient.Update(ctx, secret) - require.NoError(t, err) - - // wait for the password to be updated in Aiven - require.Eventually(t, func() bool { - finalUserAvn, err := avnGen.ServiceUserGet(ctx, cfg.Project, serviceName, userName) - if err != nil { - return false - } - return finalUserAvn.Password == "updated-cross-ns-password-67890" - }, 1*time.Minute, 5*time.Second, "Cross-namespace secret watcher should trigger password update") - - finalUserAvn, err := avnGen.ServiceUserGet(ctx, cfg.Project, serviceName, userName) - require.NoError(t, err) - assert.Equal(t, userName, finalUserAvn.Username) - - assert.Equal(t, "updated-cross-ns-password-67890", finalUserAvn.Password, "Cross-namespace password should be updated to the new value from the secret") - assert.NotEqual(t, initialPassword, finalUserAvn.Password, "Cross-namespace password should have changed from initial value") -} - // getUpdatedServiceUserAndSecretYaml returns YAML with both secret and ServiceUser changes // This simulates a user doing "kubectl apply -f config.yaml" with multiple resource changes func getUpdatedServiceUserAndSecretYaml(project, serviceName, userName, secretName, targetSecretName string) string {
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/aiven/aiven-operator/commit/032c9ba63257fdd2fddfb7f73f71830e371ff182nvdPatchWEB
- github.com/advisories/GHSA-99j8-wv67-4c72ghsaADVISORY
- github.com/aiven/aiven-operator/security/advisories/GHSA-99j8-wv67-4c72nvdVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-39961ghsaADVISORY
- github.com/aiven/aiven-operator/releases/tag/v0.37.0nvdProductRelease NotesWEB
News mentions
0No linked articles in our index yet.