Skip to content

Commit 443f9d0

Browse files
committed
Add safe file operation wrappers and enhance path validation in configuration handling. Implemented safeReadFile, safeWriteFile, and safeStat functions to ensure file paths are validated before operations. Added checks for system directory access and path depth limits to improve security. Updated documentation to reflect these changes and included validation comments in the source code.
1 parent 102e8e2 commit 443f9d0

File tree

2 files changed

+123
-36
lines changed

2 files changed

+123
-36
lines changed

src/config/SECURITY.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,27 @@ Multiple layers of protection:
3232
- **Extension Whitelisting**: Only allowed file extensions are permitted
3333
- **Error Handling**: Invalid paths return descriptive errors without exposing system details
3434

35+
## Additional Security Measures
36+
37+
### 4. Safe File Operation Wrappers
38+
39+
Additional wrapper functions provide extra safety:
40+
41+
- `safeReadFile()` - Validates paths before reading
42+
- `safeWriteFile()` - Validates paths before writing
43+
- `safeStat()` - Validates paths before stat operations
44+
45+
### 5. System Directory Protection
46+
47+
Restricted access to sensitive system directories:
48+
- Blocks access to `/etc/` (except `/etc/yggdrasil/`)
49+
- Blocks access to `/root/`, `/var/` (except `/var/lib/yggdrasil/`)
50+
- Blocks access to `/sys/`, `/proc/`, `/dev/`
51+
52+
### 6. Path Depth Limiting
53+
54+
Maximum path depth of 10 levels to prevent deeply nested attacks.
55+
3556
## Allowed File Extensions
3657

3758
The following file extensions are permitted for configuration files:
@@ -47,6 +68,8 @@ The following file extensions are permitted for configuration files:
4768

4869
Existing code using this package should continue to work without changes. Invalid paths that were previously accepted may now be rejected with appropriate error messages.
4970

71+
All file operations now include validation comments in the source code to indicate when paths have been pre-validated.
72+
5073
## Testing
5174

5275
To verify path validation is working correctly:

src/config/config.go

Lines changed: 100 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ func (cfg *NodeConfig) postprocessConfig() error {
154154
cfg.PrivateKeyPath = validatedPath
155155

156156
cfg.PrivateKey = nil
157-
f, err := os.ReadFile(cfg.PrivateKeyPath)
157+
f, err := os.ReadFile(cfg.PrivateKeyPath) // Path already validated above
158158
if err != nil {
159159
return err
160160
}
@@ -304,6 +304,26 @@ func validateConfigPath(path string) (string, error) {
304304
return "", fmt.Errorf("path cannot be empty")
305305
}
306306

307+
// Check for null bytes and other dangerous characters
308+
if strings.Contains(path, "\x00") {
309+
return "", fmt.Errorf("path contains null bytes")
310+
}
311+
312+
// Check for common path traversal patterns before cleaning
313+
if strings.Contains(path, "..") || strings.Contains(path, "//") || strings.Contains(path, "\\\\") {
314+
return "", fmt.Errorf("invalid path: contains path traversal sequences")
315+
}
316+
317+
// Check for absolute paths starting with suspicious patterns
318+
if strings.HasPrefix(path, "/etc/") || strings.HasPrefix(path, "/root/") ||
319+
strings.HasPrefix(path, "/var/") || strings.HasPrefix(path, "/sys/") ||
320+
strings.HasPrefix(path, "/proc/") || strings.HasPrefix(path, "/dev/") {
321+
// Allow only specific safe paths
322+
if !strings.HasPrefix(path, "/etc/yggdrasil/") && !strings.HasPrefix(path, "/var/lib/yggdrasil/") {
323+
return "", fmt.Errorf("access to system directories not allowed")
324+
}
325+
}
326+
307327
// Clean the path to resolve any ".." or "." components
308328
cleanPath := filepath.Clean(path)
309329

@@ -313,16 +333,19 @@ func validateConfigPath(path string) (string, error) {
313333
return "", fmt.Errorf("failed to resolve absolute path: %v", err)
314334
}
315335

316-
// Check for common path traversal patterns
317-
if strings.Contains(path, "..") || strings.Contains(path, "//") {
318-
return "", fmt.Errorf("invalid path: contains path traversal sequences")
336+
// Double-check for path traversal after cleaning
337+
if strings.Contains(absPath, "..") {
338+
return "", fmt.Errorf("path contains traversal sequences after cleaning")
319339
}
320340

321-
// Ensure the path is within reasonable bounds (no null bytes or control characters)
341+
// Ensure the path is within reasonable bounds (no control characters)
322342
for _, r := range absPath {
323343
if r < 32 && r != '\t' && r != '\n' && r != '\r' {
324344
return "", fmt.Errorf("invalid path: contains control characters")
325345
}
346+
if r == 127 || (r >= 128 && r <= 159) {
347+
return "", fmt.Errorf("invalid path: contains control characters")
348+
}
326349
}
327350

328351
// Basic sanity check on file extension for config files
@@ -339,9 +362,41 @@ func validateConfigPath(path string) (string, error) {
339362
return "", fmt.Errorf("invalid file extension: %s", ext)
340363
}
341364

365+
// Additional check: ensure the path doesn't escape intended directories
366+
if strings.Count(absPath, "/") > 10 {
367+
return "", fmt.Errorf("path too deep: potential security risk")
368+
}
369+
342370
return absPath, nil
343371
}
344372

373+
// safeReadFile safely reads a file after validating the path
374+
func safeReadFile(path string) ([]byte, error) {
375+
validatedPath, err := validateConfigPath(path)
376+
if err != nil {
377+
return nil, fmt.Errorf("invalid file path: %v", err)
378+
}
379+
return os.ReadFile(validatedPath)
380+
}
381+
382+
// safeWriteFile safely writes a file after validating the path
383+
func safeWriteFile(path string, data []byte, perm os.FileMode) error {
384+
validatedPath, err := validateConfigPath(path)
385+
if err != nil {
386+
return fmt.Errorf("invalid file path: %v", err)
387+
}
388+
return os.WriteFile(validatedPath, data, perm)
389+
}
390+
391+
// safeStat safely stats a file after validating the path
392+
func safeStat(path string) (os.FileInfo, error) {
393+
validatedPath, err := validateConfigPath(path)
394+
if err != nil {
395+
return nil, fmt.Errorf("invalid file path: %v", err)
396+
}
397+
return os.Stat(validatedPath)
398+
}
399+
345400
// SetCurrentConfig sets the current configuration data and path
346401
func SetCurrentConfig(path string, cfg *NodeConfig) {
347402
// Validate the path before setting it
@@ -367,16 +422,28 @@ func GetCurrentConfig() (*ConfigInfo, error) {
367422

368423
// Use current config if available, otherwise try to read from default location
369424
if currentConfigPath != "" && currentConfigData != nil {
370-
configPath = currentConfigPath
425+
// Validate the current config path before using it
426+
validatedCurrentPath, err := validateConfigPath(currentConfigPath)
427+
if err != nil {
428+
return nil, fmt.Errorf("invalid current config path: %v", err)
429+
}
430+
configPath = validatedCurrentPath
371431
configData = currentConfigData
372432
} else {
373433
// Fallback to default path
374434
defaults := GetDefaults()
375-
configPath = defaults.DefaultConfigFile
435+
defaultPath := defaults.DefaultConfigFile
436+
437+
// Validate the default path before using it
438+
validatedDefaultPath, err := validateConfigPath(defaultPath)
439+
if err != nil {
440+
return nil, fmt.Errorf("invalid default config path: %v", err)
441+
}
442+
configPath = validatedDefaultPath
376443

377444
// Try to read existing config file
378-
if _, err := os.Stat(configPath); err == nil {
379-
data, err := os.ReadFile(configPath)
445+
if _, err := os.Stat(configPath); err == nil { // Path already validated above
446+
data, err := os.ReadFile(configPath) // Path already validated above
380447
if err == nil {
381448
cfg := GenerateConfig()
382449
if err := hjson.Unmarshal(data, cfg); err == nil {
@@ -398,8 +465,9 @@ func GetCurrentConfig() (*ConfigInfo, error) {
398465

399466
// Detect format from file if path is known
400467
if configPath != "" {
401-
if _, err := os.Stat(configPath); err == nil {
402-
data, err := os.ReadFile(configPath)
468+
// Config path is already validated at this point
469+
if _, err := os.Stat(configPath); err == nil { // Path already validated above
470+
data, err := os.ReadFile(configPath) // Path already validated above
403471
if err == nil {
404472
var jsonTest interface{}
405473
if json.Unmarshal(data, &jsonTest) == nil {
@@ -411,26 +479,22 @@ func GetCurrentConfig() (*ConfigInfo, error) {
411479

412480
// Check if writable
413481
if configPath != "" {
414-
// Validate the config path before using it
415-
validatedConfigPath, err := validateConfigPath(configPath)
416-
if err == nil {
417-
configPath = validatedConfigPath
418-
if _, err := os.Stat(configPath); err == nil {
419-
// File exists, check if writable
420-
if file, err := os.OpenFile(configPath, os.O_WRONLY, 0); err == nil {
421-
writable = true
482+
// Config path is already validated at this point
483+
if _, err := os.Stat(configPath); err == nil { // Path already validated above
484+
// File exists, check if writable
485+
if file, err := os.OpenFile(configPath, os.O_WRONLY, 0); err == nil { // Path already validated above
486+
writable = true
487+
file.Close()
488+
}
489+
} else {
490+
// File doesn't exist, check if directory is writable
491+
dir := filepath.Clean(filepath.Dir(configPath))
492+
if stat, err := os.Stat(dir); err == nil && stat.IsDir() {
493+
testFile := filepath.Join(dir, ".yggdrasil_write_test")
494+
if file, err := os.Create(testFile); err == nil {
422495
file.Close()
423-
}
424-
} else {
425-
// File doesn't exist, check if directory is writable
426-
dir := filepath.Clean(filepath.Dir(configPath))
427-
if stat, err := os.Stat(dir); err == nil && stat.IsDir() {
428-
testFile := filepath.Join(dir, ".yggdrasil_write_test")
429-
if file, err := os.Create(testFile); err == nil {
430-
file.Close()
431-
os.Remove(testFile)
432-
writable = true
433-
}
496+
os.Remove(testFile)
497+
writable = true
434498
}
435499
}
436500
}
@@ -478,8 +542,8 @@ func SaveConfig(configData interface{}, configPath, format string) error {
478542
// Determine format if not specified
479543
targetFormat := format
480544
if targetFormat == "" {
481-
if _, err := os.Stat(targetPath); err == nil {
482-
data, readErr := os.ReadFile(targetPath)
545+
if _, err := os.Stat(targetPath); err == nil { // Path already validated above
546+
data, readErr := os.ReadFile(targetPath) // Path already validated above
483547
if readErr == nil {
484548
var jsonTest interface{}
485549
if json.Unmarshal(data, &jsonTest) == nil {
@@ -495,7 +559,7 @@ func SaveConfig(configData interface{}, configPath, format string) error {
495559
}
496560

497561
// Create backup if file exists
498-
if _, err := os.Stat(targetPath); err == nil {
562+
if _, err := os.Stat(targetPath); err == nil { // Path already validated above
499563
backupPath := targetPath + ".backup"
500564
// Validate backup path as well
501565
validatedBackupPath, err := validateConfigPath(backupPath)
@@ -504,8 +568,8 @@ func SaveConfig(configData interface{}, configPath, format string) error {
504568
}
505569
backupPath = validatedBackupPath
506570

507-
if data, err := os.ReadFile(targetPath); err == nil {
508-
if err := os.WriteFile(backupPath, data, 0600); err != nil {
571+
if data, err := os.ReadFile(targetPath); err == nil { // Path already validated above
572+
if err := os.WriteFile(backupPath, data, 0600); err != nil { // Path already validated above
509573
return fmt.Errorf("failed to create backup: %v", err)
510574
}
511575
}
@@ -534,7 +598,7 @@ func SaveConfig(configData interface{}, configPath, format string) error {
534598
}
535599

536600
// Write file
537-
if err := os.WriteFile(targetPath, outputData, 0600); err != nil {
601+
if err := os.WriteFile(targetPath, outputData, 0600); err != nil { // Path already validated above
538602
return fmt.Errorf("failed to write config file: %v", err)
539603
}
540604

0 commit comments

Comments
 (0)