Skip to content

Commit e615440

Browse files
authored
Prepare v2.17.3 (#7333)
* Merge commit from fork * fix: projected service accounts are validated to prevent arbitrary path reads Signed-off-by: Jorge Turrado <jorge_turrado@hotmail.es> * validate signature Signed-off-by: Jorge Turrado <jorge_turrado@hotmail.es> --------- Signed-off-by: Jorge Turrado <jorge_turrado@hotmail.es> * update changelog Signed-off-by: Jorge Turrado <jorge.turrado@mail.schwarz> * Update releases Signed-off-by: Jorge Turrado <jorge.turrado@mail.schwarz> --------- Signed-off-by: Jorge Turrado <jorge_turrado@hotmail.es> Signed-off-by: Jorge Turrado <jorge.turrado@mail.schwarz>
1 parent b51655b commit e615440

File tree

4 files changed

+219
-34
lines changed

4 files changed

+219
-34
lines changed

CHANGELOG.md

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ To learn more about active deprecations, we recommend checking [GitHub Discussio
1515

1616
## History
1717

18-
- [Unreleased](#unreleased)
18+
- [v2.17.3](#v2173)
1919
- [v2.17.2](#v2172)
2020
- [v2.17.1](#v2171)
2121
- [v2.17.0](#v2170)
@@ -58,39 +58,11 @@ To learn more about active deprecations, we recommend checking [GitHub Discussio
5858
- [v1.1.0](#v110)
5959
- [v1.0.0](#v100)
6060

61-
## Unreleased
62-
63-
### New
64-
65-
- TODO ([#XXX](https://github.com/kedacore/keda/issues/XXX))
66-
67-
#### Experimental
68-
69-
- TODO ([#XXX](https://github.com/kedacore/keda/issues/XXX))
70-
71-
### Improvements
72-
73-
- TODO ([#XXX](https://github.com/kedacore/keda/issues/XXX))
61+
## v2.17.3
7462

7563
### Fixes
7664

77-
- TODO ([#XXX](https://github.com/kedacore/keda/issues/XXX))
78-
79-
### Deprecations
80-
81-
You can find all deprecations in [this overview](https://github.com/kedacore/keda/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Abreaking-change) and [join the discussion here](https://github.com/kedacore/keda/discussions/categories/deprecations).
82-
83-
New deprecation(s):
84-
85-
- TODO ([#XXX](https://github.com/kedacore/keda/issues/XXX))
86-
87-
### Breaking Changes
88-
89-
- TODO ([#XXX](https://github.com/kedacore/keda/issues/XXX))
90-
91-
### Other
92-
93-
- TODO ([#XXX](https://github.com/kedacore/keda/issues/XXX))
65+
- **General**: Fix CVE-2025-68476 ([#7333](https://github.com/kedacore/keda/pull/7333))
9466

9567
## v2.17.2
9668

pkg/scaling/resolver/hashicorpvault_handler.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
"encoding/json"
2121
"errors"
2222
"fmt"
23-
"os"
2423
"strings"
2524

2625
"github.com/go-logr/logr"
@@ -120,8 +119,7 @@ func (vh *HashicorpVaultHandler) token(client *vaultapi.Client) (string, error)
120119
return token, errors.New("k8s SA file not in config")
121120
}
122121

123-
// Get the JWT from POD
124-
jwt, err := os.ReadFile(vh.vault.Credential.ServiceAccount)
122+
jwt, err := readKubernetesServiceAccountProjectedToken(vh.vault.Credential.ServiceAccount)
125123
if err != nil {
126124
return token, err
127125
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package resolver
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
8+
"github.com/golang-jwt/jwt/v5"
9+
)
10+
11+
var parser = jwt.NewParser()
12+
13+
func readKubernetesServiceAccountProjectedToken(path string) ([]byte, error) {
14+
jwt, err := os.ReadFile(path)
15+
if err != nil {
16+
return []byte{}, err
17+
}
18+
if err = validateK8sSAToken(jwt); err != nil {
19+
return []byte{}, err
20+
}
21+
return jwt, nil
22+
}
23+
24+
func validateK8sSAToken(saToken []byte) error {
25+
claims := jwt.MapClaims{}
26+
_, _, err := parser.ParseUnverified(string(saToken), &claims)
27+
if err != nil {
28+
return fmt.Errorf("error validating token: %w", err)
29+
}
30+
sub, err := claims.GetSubject()
31+
if err != nil {
32+
return fmt.Errorf("error getting token sub: %w", err)
33+
}
34+
if !strings.HasPrefix(sub, "system:serviceaccount:") {
35+
return fmt.Errorf("error validating token: subject isn't a service account")
36+
}
37+
38+
return nil
39+
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/*
2+
Copyright 2025 The KEDA Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package resolver
18+
19+
import (
20+
"crypto/rand"
21+
"crypto/rsa"
22+
"crypto/x509"
23+
"encoding/pem"
24+
"os"
25+
"testing"
26+
"time"
27+
28+
"github.com/golang-jwt/jwt/v5"
29+
)
30+
31+
func TestReadKubernetesServiceAccountProjectedToken(t *testing.T) {
32+
tests := []struct {
33+
name string
34+
setupToken func() string
35+
expectError bool
36+
validate func([]byte) bool
37+
}{
38+
{
39+
name: "valid token",
40+
setupToken: func() string {
41+
privateKey, _, err := generateTestRSAKeyPair()
42+
if err != nil {
43+
t.Fatalf("failed to generate RSA keys: %v", err)
44+
}
45+
46+
// Create valid JWT token
47+
claims := jwt.MapClaims{
48+
"iss": "kubernetes/serviceaccount",
49+
"sub": "system:serviceaccount:default:default",
50+
"exp": time.Now().Add(time.Hour).Unix(),
51+
"iat": time.Now().Unix(),
52+
}
53+
tokenBytes, err := createJWTToken(privateKey, claims)
54+
if err != nil {
55+
t.Fatalf("failed to create JWT token: %v", err)
56+
}
57+
tokenPath := createTempFile(t, tokenBytes)
58+
59+
return tokenPath
60+
},
61+
expectError: false,
62+
validate: func(token []byte) bool {
63+
return len(token) > 0
64+
},
65+
},
66+
{
67+
name: "token file does not exist",
68+
setupToken: func() string {
69+
return "/nonexistent/token/path"
70+
},
71+
expectError: true,
72+
},
73+
{
74+
name: "arbitrary file content is not a valid token",
75+
setupToken: func() string {
76+
// Create an arbitrary file with random content that is not a JWT
77+
arbitraryContent := []byte("This is just arbitrary file content, not a JWT token at all")
78+
tokenPath := createTempFile(t, arbitraryContent)
79+
80+
return tokenPath
81+
},
82+
expectError: true,
83+
},
84+
{
85+
name: "not sa token",
86+
setupToken: func() string {
87+
privateKey, _, err := generateTestRSAKeyPair()
88+
if err != nil {
89+
t.Fatalf("failed to generate RSA keys: %v", err)
90+
}
91+
92+
// Create valid JWT token but not from k8s
93+
claims := jwt.MapClaims{
94+
"iss": "random-issuer",
95+
"sub": "1234-3212",
96+
"exp": time.Now().Add(time.Hour).Unix(),
97+
"iat": time.Now().Unix(),
98+
}
99+
tokenBytes, err := createJWTToken(privateKey, claims)
100+
tokenPath := createTempFile(t, tokenBytes)
101+
102+
return tokenPath
103+
},
104+
expectError: true,
105+
},
106+
}
107+
108+
for _, tt := range tests {
109+
t.Run(tt.name, func(t *testing.T) {
110+
tokenPath := tt.setupToken()
111+
defer os.Remove(tokenPath)
112+
113+
result, err := readKubernetesServiceAccountProjectedToken(tokenPath)
114+
115+
if (err != nil) != tt.expectError {
116+
t.Errorf("readKubernetesServiceAccountProjectedToken() error = %v, expectError = %v", err, tt.expectError)
117+
return
118+
}
119+
120+
if !tt.expectError && tt.validate != nil {
121+
if !tt.validate(result) {
122+
t.Errorf("readKubernetesServiceAccountProjectedToken() returned invalid result")
123+
}
124+
}
125+
})
126+
}
127+
}
128+
129+
// Helper function to generate RSA key pair for testing
130+
func generateTestRSAKeyPair() (*rsa.PrivateKey, *rsa.PublicKey, error) {
131+
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
132+
if err != nil {
133+
return nil, nil, err
134+
}
135+
return privateKey, &privateKey.PublicKey, nil
136+
}
137+
138+
// Helper function to create a valid JWT token for testing
139+
func createJWTToken(privateKey *rsa.PrivateKey, claims jwt.MapClaims) ([]byte, error) {
140+
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
141+
tokenString, err := token.SignedString(privateKey)
142+
if err != nil {
143+
return nil, err
144+
}
145+
return []byte(tokenString), nil
146+
}
147+
148+
// Helper function to write RSA public key to PEM format
149+
func writePublicKeyPEM(publicKey *rsa.PublicKey) ([]byte, error) {
150+
publicKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey)
151+
if err != nil {
152+
return nil, err
153+
}
154+
155+
publicKeyPEM := pem.EncodeToMemory(&pem.Block{
156+
Type: "PUBLIC KEY",
157+
Bytes: publicKeyBytes,
158+
})
159+
160+
return publicKeyPEM, nil
161+
}
162+
163+
// Helper function to create temporary files for testing
164+
func createTempFile(t *testing.T, content []byte) string {
165+
tmpFile, err := os.CreateTemp("", "k8s_test_*")
166+
if err != nil {
167+
t.Fatalf("failed to create temp file: %v", err)
168+
}
169+
defer tmpFile.Close()
170+
171+
if _, err := tmpFile.Write(content); err != nil {
172+
t.Fatalf("failed to write to temp file: %v", err)
173+
}
174+
175+
return tmpFile.Name()
176+
}

0 commit comments

Comments
 (0)