@@ -6,11 +6,17 @@ import (
66 "os/exec"
77 "path/filepath"
88 "strings"
9+ "syscall"
910
1011 "github.com/PythonHacker24/linux-acl-management-backend/config"
1112 "go.uber.org/zap"
1213)
1314
15+ /* comprehensive list of dangerous characters */
16+ var (
17+ dangerousChars = []string {";" , "|" , "&" , "`" , "$" , "(" , ")" , "<" , ">" , "{" , "}" , "[" , "]" , "\\ " , "'" , "\" " }
18+ )
19+
1420/* list files in a given directory with some basic information */
1521func ListFiles (path string , userID string ) ([]FileEntry , error ) {
1622 var entries []FileEntry
@@ -21,43 +27,101 @@ func ListFiles(path string, userID string) ([]FileEntry, error) {
2127 /* clean the path to prevent directory traversal */
2228 fullPath = filepath .Clean (fullPath )
2329
30+ /* evaluate symlinks to get the real path */
31+ realPath , err := filepath .EvalSymlinks (fullPath )
32+ if err != nil {
33+ zap .L ().Warn ("Failed to evaluate symlinks" ,
34+ zap .String ("path" , fullPath ),
35+ zap .Error (err ),
36+ )
37+ return nil , fmt .Errorf ("invalid path or broken symlink: %w" , err )
38+ }
39+
2440 /* ensure the resulting path is still within the basePath (prevent directory traversal) */
25- if ! strings .HasPrefix (fullPath , filepath .Clean (config .BackendConfig .AppInfo .BasePath )) {
26- return nil , fmt .Errorf ("Path traversal attempt detected: %s" , path )
41+ if ! strings .HasPrefix (realPath , filepath .Clean (config .BackendConfig .AppInfo .BasePath )) {
42+ zap .L ().Warn ("Path traversal attempt detected" ,
43+ zap .String ("path" , path ),
44+ zap .String ("resolved_path" , realPath ),
45+ )
46+ return nil , fmt .Errorf ("access denied: path outside allowed directory" )
2747 }
2848
2949 /* list all the files in the given directory */
30- files , err := os .ReadDir (fullPath )
50+ files , err := os .ReadDir (realPath )
3151 if err != nil {
32- return nil , err
52+ zap .L ().Error ("Failed to read directory" ,
53+ zap .String ("path" , realPath ),
54+ zap .Error (err ),
55+ )
56+ return nil , fmt .Errorf ("failed to read directory: %w" , err )
3357 }
3458
35- /* retrive information for each file in the directory */
59+ /* retrieve information for each file in the directory */
3660 for _ , f := range files {
37- fullPath := filepath .Join (path , f .Name ())
61+ entryPath := filepath .Join (path , f .Name ())
62+ fullEntryPath := filepath .Join (realPath , f .Name ())
3863
39- /* check ACL access first */
40- isOwner , err := isOwner ( fullPath , userID )
64+ /* evaluate symlinks for each entry */
65+ realEntryPath , err := filepath . EvalSymlinks ( fullEntryPath )
4166 if err != nil {
42- return nil , fmt .Errorf ("error during listing files: %w" , err )
67+ zap .L ().Warn ("Failed to evaluate symlinks for entry" ,
68+ zap .String ("entry" , f .Name ()),
69+ zap .Error (err ),
70+ )
71+ continue
72+ }
73+
74+ /* verify the entry is still within allowed directory */
75+ if ! strings .HasPrefix (realEntryPath , filepath .Clean (config .BackendConfig .AppInfo .BasePath )) {
76+ zap .L ().Warn ("Entry symlink points outside allowed directory" ,
77+ zap .String ("entry" , f .Name ()),
78+ zap .String ("resolved_path" , realEntryPath ),
79+ )
80+ continue
81+ }
82+
83+ /* Open the file with O_NOFOLLOW to prevent symlink races */
84+ file , err := os .OpenFile (realEntryPath , os .O_RDONLY | syscall .O_NOFOLLOW , 0 )
85+ if err != nil {
86+ zap .L ().Warn ("Failed to open file" ,
87+ zap .String ("path" , realEntryPath ),
88+ zap .Error (err ),
89+ )
90+ continue
91+ }
92+ defer file .Close ()
93+
94+ /* Get file descriptor for further operations */
95+ fd := file .Fd ()
96+
97+ /* check ACL access using the file descriptor */
98+ isOwner , err := isOwnerFd (fd , realEntryPath , userID )
99+ if err != nil {
100+ zap .L ().Error ("Failed to check ownership" ,
101+ zap .String ("path" , realEntryPath ),
102+ zap .Error (err ),
103+ )
104+ continue
43105 }
44106
45107 if ! isOwner {
46- /* if the user doesn't have right ACL permissions for the file, skip it */
47108 continue
48109 }
49110
50- /* get information about the file */
51- info , err := f . Info ()
111+ /* get file information using the same file descriptor */
112+ info , err := file . Stat ()
52113 if err != nil {
114+ zap .L ().Warn ("Error while getting file information" ,
115+ zap .String ("path" , realEntryPath ),
116+ zap .Error (err ),
117+ )
53118 continue
54119 }
55120
56- /* store it in entries that would be returned */
57121 entries = append (entries , FileEntry {
58122 Name : f .Name (),
59- Path : fullPath ,
60- IsDir : f .IsDir (),
123+ Path : entryPath ,
124+ IsDir : info .IsDir (),
61125 Size : info .Size (),
62126 ModTime : info .ModTime ().Unix (),
63127 })
@@ -67,53 +131,51 @@ func ListFiles(path string, userID string) ([]FileEntry, error) {
67131}
68132
69133/*
70- checks if the user is the owner of the file
71- username is the LDAP CN for the user
72- uses getfacl to fetch the permissions (usually from filesystems mounted from remote servers)
134+ checks if the user is the owner of the file using a file descriptor
135+ this reduces race conditions by operating on an already-open file
73136*/
74- func isOwner (filePath string , userCN string ) (bool , error ) {
75-
137+ func isOwnerFd (fd uintptr , filePath string , userCN string ) (bool , error ) {
76138 cleanPath := filepath .Clean (filePath )
77139
78- /* additional validation to ensure that the path doesn't contain dangerous characters */
79- if strings .Contains (cleanPath , ";" ) || strings .Contains (cleanPath , "|" ) ||
80- strings .Contains (cleanPath , "&" ) || strings .Contains (cleanPath , "`" ) ||
81- strings .Contains (cleanPath , "$" ) || strings .Contains (cleanPath , "(" ) ||
82- strings .Contains (cleanPath , ")" ) {
83- zap .L ().Warn ("Illegal method attempted while getting file path by injecting dangerous character in the file path!" )
84- return false , fmt .Errorf ("invalid characters in file path: %s" , cleanPath )
140+ /* validation to ensure that the path doesn't contain dangerous characters */
141+ for _ , char := range dangerousChars {
142+ if strings .Contains (cleanPath , char ) {
143+ zap .L ().Warn ("Illegal character detected in file path" ,
144+ zap .String ("path" , cleanPath ),
145+ zap .String ("character" , char ),
146+ )
147+ return false , fmt .Errorf ("invalid character in file path" )
148+ }
85149 }
86150
87- /* get the file's ACL using getfacl with properly escaped arguments */
88- cmd := exec .Command ("getfacl" , "-- " , cleanPath )
151+ /* get the file's ACL using getfacl with the file descriptor */
152+ cmd := exec .Command ("getfacl" , fmt . Sprintf ( "/proc/self/fd/%d " , fd ) )
89153 output , err := cmd .Output ()
90154 if err != nil {
91- return false , fmt .Errorf ("failed to execute getfacl on %s: %v" , cleanPath , err )
155+ zap .L ().Error ("Failed to execute getfacl" ,
156+ zap .String ("path" , cleanPath ),
157+ zap .Error (err ),
158+ )
159+ return false , fmt .Errorf ("failed to check file permissions: %w" , err )
92160 }
93161
94162 /* parse the getfacl output to check ownership */
95163 lines := strings .Split (string (output ), "\n " )
96164 for _ , line := range lines {
97165 line = strings .TrimSpace (line )
98166
99- /* check for owner line (format: "# owner: username") */
100167 if strings .HasPrefix (line , "# owner:" ) {
101168 owner := strings .TrimSpace (strings .TrimPrefix (line , "# owner:" ))
102-
103- /* compare with the provided CN (case-insensitive) */
104169 if strings .EqualFold (owner , userCN ) {
105170 return true , nil
106171 }
107172 }
108173
109- /* also check user ACL entries (format: "user:username:permissions") */
110174 if strings .HasPrefix (line , "user:" ) && ! strings .HasPrefix (line , "user::" ) {
111175 parts := strings .Split (line , ":" )
112176 if len (parts ) >= 3 {
113177 aclUser := parts [1 ]
114178 permissions := parts [2 ]
115-
116- /* check if this user has write permissions (indicating ownership-like access) */
117179 if strings .EqualFold (aclUser , userCN ) && strings .Contains (permissions , "w" ) {
118180 return true , nil
119181 }
@@ -122,4 +184,4 @@ func isOwner(filePath string, userCN string) (bool, error) {
122184 }
123185
124186 return false , nil
125- }
187+ }
0 commit comments