@@ -6,6 +6,9 @@ package ca
66import (
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+ }
0 commit comments