@@ -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
346401func 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