From c805b7389a64ebd99a5463eecaf3b2d32f3480fe Mon Sep 17 00:00:00 2001 From: Azure Linux Security Servicing Account Date: Mon, 29 Dec 2025 05:22:31 +0000 Subject: [PATCH] Patch keda for CVE-2025-68476 --- SPECS/keda/CVE-2025-68476.patch | 296 ++++++++++++++++++++++++++++++++ SPECS/keda/keda.spec | 6 +- 2 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 SPECS/keda/CVE-2025-68476.patch diff --git a/SPECS/keda/CVE-2025-68476.patch b/SPECS/keda/CVE-2025-68476.patch new file mode 100644 index 00000000000..7d1f412ff3f --- /dev/null +++ b/SPECS/keda/CVE-2025-68476.patch @@ -0,0 +1,296 @@ +From ba564be057a39dc6b72cf659ffe93d519ac61b67 Mon Sep 17 00:00:00 2001 +From: Jorge Turrado Ferrero +Date: Mon, 22 Dec 2025 11:20:55 +0100 +Subject: [PATCH 1/3] Merge commit from fork + +* fix: projected service accounts are validated to prevent arbitrary path reads + +Signed-off-by: Jorge Turrado + +* validate signature + +Signed-off-by: Jorge Turrado + +--------- + +Signed-off-by: Jorge Turrado +--- + .../resolver/hashicorpvault_handler.go | 4 +- + pkg/scaling/resolver/k8s_validator.go | 39 ++++ + pkg/scaling/resolver/k8s_validator_test.go | 176 ++++++++++++++++++ + 3 files changed, 216 insertions(+), 3 deletions(-) + create mode 100644 pkg/scaling/resolver/k8s_validator.go + create mode 100644 pkg/scaling/resolver/k8s_validator_test.go + +diff --git a/pkg/scaling/resolver/hashicorpvault_handler.go b/pkg/scaling/resolver/hashicorpvault_handler.go +index 1781a204..15b2846e 100644 +--- a/pkg/scaling/resolver/hashicorpvault_handler.go ++++ b/pkg/scaling/resolver/hashicorpvault_handler.go +@@ -20,7 +20,6 @@ import ( + "encoding/json" + "errors" + "fmt" +- "os" + "strings" + + "github.com/go-logr/logr" +@@ -120,8 +119,7 @@ func (vh *HashicorpVaultHandler) token(client *vaultapi.Client) (string, error) + return token, errors.New("k8s SA file not in config") + } + +- // Get the JWT from POD +- jwt, err := os.ReadFile(vh.vault.Credential.ServiceAccount) ++ jwt, err := readKubernetesServiceAccountProjectedToken(vh.vault.Credential.ServiceAccount) + if err != nil { + return token, err + } +diff --git a/pkg/scaling/resolver/k8s_validator.go b/pkg/scaling/resolver/k8s_validator.go +new file mode 100644 +index 00000000..f32a0c36 +--- /dev/null ++++ b/pkg/scaling/resolver/k8s_validator.go +@@ -0,0 +1,39 @@ ++package resolver ++ ++import ( ++ "fmt" ++ "os" ++ "strings" ++ ++ "github.com/golang-jwt/jwt/v5" ++) ++ ++var parser = jwt.NewParser() ++ ++func readKubernetesServiceAccountProjectedToken(path string) ([]byte, error) { ++ jwt, err := os.ReadFile(path) ++ if err != nil { ++ return []byte{}, err ++ } ++ if err = validateK8sSAToken(jwt); err != nil { ++ return []byte{}, err ++ } ++ return jwt, nil ++} ++ ++func validateK8sSAToken(saToken []byte) error { ++ claims := jwt.MapClaims{} ++ _, _, err := parser.ParseUnverified(string(saToken), &claims) ++ if err != nil { ++ return fmt.Errorf("error validating token: %w", err) ++ } ++ sub, err := claims.GetSubject() ++ if err != nil { ++ return fmt.Errorf("error getting token sub: %w", err) ++ } ++ if !strings.HasPrefix(sub, "system:serviceaccount:") { ++ return fmt.Errorf("error validating token: subject isn't a service account") ++ } ++ ++ return nil ++} +diff --git a/pkg/scaling/resolver/k8s_validator_test.go b/pkg/scaling/resolver/k8s_validator_test.go +new file mode 100644 +index 00000000..0e2b2cf8 +--- /dev/null ++++ b/pkg/scaling/resolver/k8s_validator_test.go +@@ -0,0 +1,176 @@ ++/* ++Copyright 2025 The KEDA Authors ++ ++Licensed under the Apache License, Version 2.0 (the "License"); ++you may not use this file except in compliance with the License. ++You may obtain a copy of the License at ++ ++ http://www.apache.org/licenses/LICENSE-2.0 ++ ++Unless required by applicable law or agreed to in writing, software ++distributed under the License is distributed on an "AS IS" BASIS, ++WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++See the License for the specific language governing permissions and ++limitations under the License. ++*/ ++ ++package resolver ++ ++import ( ++ "crypto/rand" ++ "crypto/rsa" ++ "crypto/x509" ++ "encoding/pem" ++ "os" ++ "testing" ++ "time" ++ ++ "github.com/golang-jwt/jwt/v5" ++) ++ ++func TestReadKubernetesServiceAccountProjectedToken(t *testing.T) { ++ tests := []struct { ++ name string ++ setupToken func() string ++ expectError bool ++ validate func([]byte) bool ++ }{ ++ { ++ name: "valid token", ++ setupToken: func() string { ++ privateKey, _, err := generateTestRSAKeyPair() ++ if err != nil { ++ t.Fatalf("failed to generate RSA keys: %v", err) ++ } ++ ++ // Create valid JWT token ++ claims := jwt.MapClaims{ ++ "iss": "kubernetes/serviceaccount", ++ "sub": "system:serviceaccount:default:default", ++ "exp": time.Now().Add(time.Hour).Unix(), ++ "iat": time.Now().Unix(), ++ } ++ tokenBytes, err := createJWTToken(privateKey, claims) ++ if err != nil { ++ t.Fatalf("failed to create JWT token: %v", err) ++ } ++ tokenPath := createTempFile(t, tokenBytes) ++ ++ return tokenPath ++ }, ++ expectError: false, ++ validate: func(token []byte) bool { ++ return len(token) > 0 ++ }, ++ }, ++ { ++ name: "token file does not exist", ++ setupToken: func() string { ++ return "/nonexistent/token/path" ++ }, ++ expectError: true, ++ }, ++ { ++ name: "arbitrary file content is not a valid token", ++ setupToken: func() string { ++ // Create an arbitrary file with random content that is not a JWT ++ arbitraryContent := []byte("This is just arbitrary file content, not a JWT token at all") ++ tokenPath := createTempFile(t, arbitraryContent) ++ ++ return tokenPath ++ }, ++ expectError: true, ++ }, ++ { ++ name: "not sa token", ++ setupToken: func() string { ++ privateKey, _, err := generateTestRSAKeyPair() ++ if err != nil { ++ t.Fatalf("failed to generate RSA keys: %v", err) ++ } ++ ++ // Create valid JWT token but not from k8s ++ claims := jwt.MapClaims{ ++ "iss": "random-issuer", ++ "sub": "1234-3212", ++ "exp": time.Now().Add(time.Hour).Unix(), ++ "iat": time.Now().Unix(), ++ } ++ tokenBytes, err := createJWTToken(privateKey, claims) ++ tokenPath := createTempFile(t, tokenBytes) ++ ++ return tokenPath ++ }, ++ expectError: true, ++ }, ++ } ++ ++ for _, tt := range tests { ++ t.Run(tt.name, func(t *testing.T) { ++ tokenPath := tt.setupToken() ++ defer os.Remove(tokenPath) ++ ++ result, err := readKubernetesServiceAccountProjectedToken(tokenPath) ++ ++ if (err != nil) != tt.expectError { ++ t.Errorf("readKubernetesServiceAccountProjectedToken() error = %v, expectError = %v", err, tt.expectError) ++ return ++ } ++ ++ if !tt.expectError && tt.validate != nil { ++ if !tt.validate(result) { ++ t.Errorf("readKubernetesServiceAccountProjectedToken() returned invalid result") ++ } ++ } ++ }) ++ } ++} ++ ++// Helper function to generate RSA key pair for testing ++func generateTestRSAKeyPair() (*rsa.PrivateKey, *rsa.PublicKey, error) { ++ privateKey, err := rsa.GenerateKey(rand.Reader, 2048) ++ if err != nil { ++ return nil, nil, err ++ } ++ return privateKey, &privateKey.PublicKey, nil ++} ++ ++// Helper function to create a valid JWT token for testing ++func createJWTToken(privateKey *rsa.PrivateKey, claims jwt.MapClaims) ([]byte, error) { ++ token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) ++ tokenString, err := token.SignedString(privateKey) ++ if err != nil { ++ return nil, err ++ } ++ return []byte(tokenString), nil ++} ++ ++// Helper function to write RSA public key to PEM format ++func writePublicKeyPEM(publicKey *rsa.PublicKey) ([]byte, error) { ++ publicKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey) ++ if err != nil { ++ return nil, err ++ } ++ ++ publicKeyPEM := pem.EncodeToMemory(&pem.Block{ ++ Type: "PUBLIC KEY", ++ Bytes: publicKeyBytes, ++ }) ++ ++ return publicKeyPEM, nil ++} ++ ++// Helper function to create temporary files for testing ++func createTempFile(t *testing.T, content []byte) string { ++ tmpFile, err := os.CreateTemp("", "k8s_test_*") ++ if err != nil { ++ t.Fatalf("failed to create temp file: %v", err) ++ } ++ defer tmpFile.Close() ++ ++ if _, err := tmpFile.Write(content); err != nil { ++ t.Fatalf("failed to write to temp file: %v", err) ++ } ++ ++ return tmpFile.Name() ++} +-- +2.45.4 + + +From 41a1d6989402942b91694d8dc0703618d6040a3a Mon Sep 17 00:00:00 2001 +From: Jorge Turrado +Date: Mon, 22 Dec 2025 11:28:27 +0100 +Subject: [PATCH 2/3] update changelog + +Signed-off-by: Jorge Turrado +-- +2.45.4 + + +From d1b0691d4e67452d09ff56db9e48d5ff2aa5d6e0 Mon Sep 17 00:00:00 2001 +From: Jorge Turrado +Date: Mon, 22 Dec 2025 11:54:29 +0100 +Subject: [PATCH 3/3] Update releases + +Signed-off-by: Jorge Turrado +-- +2.45.4 + diff --git a/SPECS/keda/keda.spec b/SPECS/keda/keda.spec index cecf400e272..7e375463b65 100644 --- a/SPECS/keda/keda.spec +++ b/SPECS/keda/keda.spec @@ -1,7 +1,7 @@ Summary: Kubernetes-based Event Driven Autoscaling Name: keda Version: 2.14.1 -Release: 8%{?dist} +Release: 9%{?dist} License: ASL 2.0 Vendor: Microsoft Corporation Distribution: Azure Linux @@ -33,6 +33,7 @@ Patch7: CVE-2025-22870.patch Patch8: CVE-2024-51744.patch Patch9: CVE-2025-22872.patch Patch10: CVE-2025-68156.patch +Patch11: CVE-2025-68476.patch BuildRequires: golang >= 1.15 %description @@ -68,6 +69,9 @@ cp ./bin/keda-admission-webhooks %{buildroot}%{_bindir} %{_bindir}/%{name}-admission-webhooks %changelog +* Mon Dec 29 2025 Azure Linux Security Servicing Account - 2.14.1-9 +- Patch for CVE-2025-68476 + * Fri Dec 19 2025 Azure Linux Security Servicing Account - 2.14.1-8 - Patch for CVE-2025-68156