@@ -575,6 +575,28 @@ func (s *Server) buildListeners(port string) ([]net.Listener, error) {
575575 return listeners , nil
576576}
577577
578+ func (s * Server ) safeJoin (base , rel string ) (string , error ) {
579+ absBase , err := filepath .Abs (base )
580+ if err != nil {
581+ return "" , err
582+ }
583+
584+ // Join and clean the path
585+ joined := filepath .Join (absBase , rel )
586+
587+ // Ensure the result is still within the base directory
588+ relPath , err := filepath .Rel (absBase , joined )
589+ if err != nil {
590+ return "" , err
591+ }
592+
593+ if strings .HasPrefix (relPath , ".." ) || strings .HasPrefix (relPath , "/" ) {
594+ return "" , fmt .Errorf ("security: path traversal attempt detected" )
595+ }
596+
597+ return joined , nil
598+ }
599+
578600// multiListener fans in Accept() calls from multiple net.Listeners into one.
579601type multiListener struct {
580602 listeners []net.Listener
@@ -2656,8 +2678,22 @@ func (s *Server) handlePlayerQueue(w http.ResponseWriter, r *http.Request) {
26562678 }
26572679
26582680 if action == "add" {
2659- streamer .PushToQueue (path )
2660- logrus .Debugf ("AutoDJ %s: Queued song %s" , mount , path )
2681+ musicDir := streamer .GetMusicDir ()
2682+ rel , err := filepath .Rel (musicDir , path )
2683+ if err != nil {
2684+ http .Error (w , "Forbidden" , http .StatusForbidden )
2685+ return
2686+ }
2687+
2688+ fullPath , err := s .safeJoin (musicDir , rel )
2689+ if err != nil {
2690+ logrus .WithError (err ).Warnf ("Security: Blocked queue addition of %s for mount %s" , path , mount )
2691+ http .Error (w , "Forbidden" , http .StatusForbidden )
2692+ return
2693+ }
2694+
2695+ streamer .PushToQueue (fullPath )
2696+ logrus .Debugf ("AutoDJ %s: Queued song %s" , mount , fullPath )
26612697 } else if action == "remove" {
26622698 var index int
26632699 fmt .Sscanf (r .FormValue ("index" ), "%d" , & index )
@@ -2773,10 +2809,9 @@ func (s *Server) handlePlayerFiles(w http.ResponseWriter, r *http.Request) {
27732809 }
27742810
27752811 musicDir := streamer .GetMusicDir ()
2776- fullPath := filepath .Join (musicDir , subDir )
2777-
2778- // Security: Ensure fullPath is still within musicDir
2779- if ! strings .HasPrefix (fullPath , musicDir ) {
2812+ fullPath , err := s .safeJoin (musicDir , subDir )
2813+ if err != nil {
2814+ logrus .WithError (err ).Warnf ("Security: Blocked file browser access to %s for mount %s" , subDir , mount )
27802815 http .Error (w , "Forbidden" , http .StatusForbidden )
27812816 return
27822817 }
@@ -2868,7 +2903,21 @@ func (s *Server) handlePlayerPlaylistAction(w http.ResponseWriter, r *http.Reque
28682903 }
28692904
28702905 if action == "add" {
2871- fullPath := relPath // Already absolute from frontend now
2906+ musicDir := streamer .GetMusicDir ()
2907+ // Frontend sends absolute path, we convert to relative to validate via safeJoin
2908+ rel , err := filepath .Rel (musicDir , relPath )
2909+ if err != nil {
2910+ http .Error (w , "Forbidden" , http .StatusForbidden )
2911+ return
2912+ }
2913+
2914+ fullPath , err := s .safeJoin (musicDir , rel )
2915+ if err != nil {
2916+ logrus .WithError (err ).Warnf ("Security: Blocked playlist addition of %s for mount %s" , relPath , mount )
2917+ http .Error (w , "Forbidden" , http .StatusForbidden )
2918+ return
2919+ }
2920+
28722921 logrus .Infof ("AutoDJ %s: Attempting to add %s" , mount , fullPath )
28732922
28742923 info , err := os .Stat (fullPath )
0 commit comments