Skip to content

Commit c7a9f5b

Browse files
Worked on solving security issues and raceconditions in traversal code
1 parent 014d60b commit c7a9f5b

File tree

3 files changed

+100
-43
lines changed

3 files changed

+100
-43
lines changed

config/backend_security.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func (b *BackendSecurity) Normalize() error {
2323
}
2424

2525
if b.JWTExpiry == 0 {
26-
b.JWTExpiry = 1
26+
b.JWTExpiry = 24
2727
}
2828

2929
return nil

internal/auth/auth.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,6 @@ import (
1414
func GenerateJWT(username string) (string, error) {
1515
expiryHours := config.BackendConfig.BackendSecurity.JWTExpiry
1616

17-
/* GET THIS INTO CONFIG SANITISATION */
18-
if expiryHours == 0 {
19-
expiryHours = 24
20-
}
21-
2217
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
2318
"username": username,
2419
"exp": time.Now().Add(time.Hour * time.Duration(expiryHours)).Unix(),

internal/traversal/traversal.go

Lines changed: 99 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -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 */
1521
func 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

Comments
 (0)