Skip to content

Commit a989831

Browse files
Backport of Fix for arbitrary file reads vulnerabilities into release/1.22.x (#23252)
backport of commit 7054efd Co-authored-by: santoshpulluri <santosh.pulluri@hashicorp.com>
1 parent 803992d commit a989831

6 files changed

Lines changed: 566 additions & 39 deletions

File tree

.changelog/23249.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:security
2+
security: Fixed arbitrary file read vulnerability in Vault CA provider authentication methods (Kubernetes, JWT, and AppRole) by implementing OS-level path traversal protection using `os.OpenRoot()` to restrict file access to standard secret directories. This resolves the CVE-2026-2808
3+
```

agent/connect/ca/provider_vault_auth.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ package ca
66
import (
77
"context"
88
"fmt"
9+
"os"
10+
"path/filepath"
11+
"strings"
912

1013
"github.com/hashicorp/vault/api"
1114

@@ -92,3 +95,69 @@ func legacyCheck(params map[string]any, expectedKeys ...string) bool {
9295
}
9396
return false
9497
}
98+
99+
// readVaultCredentialFileSecurely reads a Vault credential file using os.OpenRoot to prevent
100+
// path traversal and symlink attacks. This provides OS-level enforcement of file system boundaries.
101+
//
102+
// Parameters:
103+
// - filePath: the path to the credential file
104+
// - allowedDirs: a list of allowed base directories where credential files can reside
105+
//
106+
// Returns the file contents or an error if the file is outside allowed directories or cannot be read.
107+
func readVaultCredentialFileSecurely(filePath string, allowedDirs []string) ([]byte, error) {
108+
// Clean and normalize the input path to remove . and .. elements
109+
cleanPath := filepath.Clean(filePath)
110+
111+
// Determine which allowed directory contains the path
112+
var baseDir string
113+
var relPath string
114+
115+
for _, dir := range allowedDirs {
116+
// Clean the allowed directory path as well
117+
cleanDir := filepath.Clean(dir)
118+
119+
// Use filepath.Rel to properly determine if path is within this directory
120+
rel, err := filepath.Rel(cleanDir, cleanPath)
121+
if err != nil {
122+
// filepath.Rel failed, skip this directory
123+
continue
124+
}
125+
126+
// If the relative path starts with "..", the path is outside this directory
127+
// If it equals ".", the path is the directory itself (not a file)
128+
// Otherwise, it's a file/subdirectory within the allowed directory
129+
if !strings.HasPrefix(rel, "..") && rel != "." {
130+
baseDir = cleanDir
131+
relPath = rel
132+
break
133+
}
134+
}
135+
136+
// If no allowed directory matches, reject the path
137+
if baseDir == "" {
138+
return nil, fmt.Errorf("credential file must be within allowed directories")
139+
}
140+
141+
// Use os.OpenRoot to create a rooted file system restricted to the base directory.
142+
// This provides OS-level protection against symlink escapes and directory traversal,
143+
// as any symlinks within the rooted filesystem cannot escape the root boundary.
144+
root, err := os.OpenRoot(baseDir)
145+
if err != nil {
146+
return nil, fmt.Errorf("failed to open root directory")
147+
}
148+
defer root.Close()
149+
150+
// Read the credential file within the rooted file system
151+
// ReadFile will fail appropriately for directories or special files
152+
fileBytes, err := root.ReadFile(relPath)
153+
if err != nil {
154+
return nil, fmt.Errorf("failed to read credential file")
155+
}
156+
157+
// Validate file size to prevent DoS attacks
158+
if len(fileBytes) > 5*1024*1024 { // 5 MB
159+
return nil, fmt.Errorf("credential file exceeds maximum allowed 5MB size")
160+
}
161+
162+
return fileBytes, nil
163+
}

agent/connect/ca/provider_vault_auth_approle.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package ca
66
import (
77
"bytes"
88
"fmt"
9-
"os"
109
"strings"
1110

1211
"github.com/hashicorp/consul/agent/structs"
@@ -37,6 +36,7 @@ func NewAppRoleAuthClient(authMethod *structs.VaultAuthMethod) (*VaultAuthClient
3736
return authClient, nil
3837
}
3938

39+
// ArLoginDataGen generates the login data for the AppRole auth method
4040
func ArLoginDataGen(authMethod *structs.VaultAuthMethod) (map[string]any, error) {
4141
// don't need to check for legacy params as this func isn't used in that case
4242
params := authMethod.Params
@@ -52,12 +52,25 @@ func ArLoginDataGen(authMethod *structs.VaultAuthMethod) (map[string]any, error)
5252
var err error
5353
var rawRoleID, rawSecretID []byte
5454
data := make(map[string]any)
55-
if rawRoleID, err = os.ReadFile(roleIdFilePath); err != nil {
55+
56+
// Define allowed base directories for AppRole credentials
57+
allowedDirs := []string{
58+
"/var/run/secrets/vault",
59+
"/run/secrets/vault",
60+
"/var/run/secrets",
61+
"/run/secrets",
62+
}
63+
64+
// Securely read the role_id file using os.OpenRoot to prevent path traversal attacks
65+
if rawRoleID, err = readVaultCredentialFileSecurely(roleIdFilePath, allowedDirs); err != nil {
5666
return nil, err
5767
}
58-
data["role_id"] = string(rawRoleID)
68+
// Trim whitespace for consistency with secret_id handling
69+
data["role_id"] = strings.TrimSpace(string(rawRoleID))
70+
5971
if hasSecret {
60-
switch rawSecretID, err = os.ReadFile(secretIdFilePath); {
72+
// Securely read the secret_id file using os.OpenRoot to prevent path traversal attacks
73+
switch rawSecretID, err = readVaultCredentialFileSecurely(secretIdFilePath, allowedDirs); {
6174
case err != nil:
6275
return nil, err
6376
case len(bytes.TrimSpace(rawSecretID)) > 0:

agent/connect/ca/provider_vault_auth_jwt.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ package ca
55

66
import (
77
"fmt"
8-
"os"
98
"strings"
109

1110
"github.com/hashicorp/consul/agent/structs"
@@ -36,12 +35,25 @@ func NewJwtAuthClient(authMethod *structs.VaultAuthMethod) (*VaultAuthClient, er
3635
return authClient, nil
3736
}
3837

38+
// JwtLoginDataGen generates the login data for the JWT auth method
3939
func JwtLoginDataGen(authMethod *structs.VaultAuthMethod) (map[string]any, error) {
4040
params := authMethod.Params
4141
role := params["role"].(string)
4242

4343
tokenPath := params["path"].(string)
44-
rawToken, err := os.ReadFile(tokenPath)
44+
45+
// Define allowed base directories for JWT credentials
46+
allowedDirs := []string{
47+
"/var/run/secrets/kubernetes.io/serviceaccount",
48+
"/var/run/secrets/vault",
49+
"/run/secrets/vault",
50+
"/var/run/secrets",
51+
"/run/secrets",
52+
}
53+
54+
// Securely read the JWT file using os.OpenRoot to prevent path traversal attacks
55+
rawToken, err := readVaultCredentialFileSecurely(tokenPath, allowedDirs)
56+
4557
if err != nil {
4658
return nil, err
4759
}

agent/connect/ca/provider_vault_auth_k8s.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ package ca
55

66
import (
77
"fmt"
8-
"os"
98
"strings"
109

1110
"github.com/hashicorp/consul/agent/structs"
@@ -30,6 +29,7 @@ func NewK8sAuthClient(authMethod *structs.VaultAuthMethod) (*VaultAuthClient, er
3029
return authClient, nil
3130
}
3231

32+
// K8sLoginDataGen generates the login data for the Kubernetes auth method
3333
func K8sLoginDataGen(authMethod *structs.VaultAuthMethod) (map[string]any, error) {
3434
params := authMethod.Params
3535
role := params["role"].(string)
@@ -39,7 +39,15 @@ func K8sLoginDataGen(authMethod *structs.VaultAuthMethod) (map[string]any, error
3939
if !ok || strings.TrimSpace(tokenPath) == "" {
4040
tokenPath = defaultK8SServiceAccountTokenPath
4141
}
42-
rawToken, err := os.ReadFile(tokenPath)
42+
43+
// Define allowed base directories for Kubernetes service account tokens
44+
allowedDirs := []string{
45+
"/var/run/secrets/kubernetes.io/serviceaccount",
46+
"/run/secrets/kubernetes.io/serviceaccount",
47+
}
48+
49+
// Securely read the JWT file using os.OpenRoot to prevent path traversal attacks
50+
rawToken, err := readVaultCredentialFileSecurely(tokenPath, allowedDirs)
4351
if err != nil {
4452
return nil, err
4553
}

0 commit comments

Comments
 (0)