@@ -525,3 +525,344 @@ func TestValidatePathWithSymlinkResolution(t *testing.T) {
525525 }
526526 })
527527}
528+
529+ // TestSymlinkTraversalAttacks tests various symlink-based path traversal attack patterns
530+ func TestSymlinkTraversalAttacks (t * testing.T ) {
531+ t .Run ("relative symlink with parent traversal should be rejected" , func (t * testing.T ) {
532+ tmpDir := t .TempDir ()
533+ workspaceDir := filepath .Join (tmpDir , "workspace" )
534+ outsideDir := filepath .Join (tmpDir , "outside" )
535+ secretFile := filepath .Join (outsideDir , "secret.txt" )
536+
537+ // Create directory structure
538+ if err := os .MkdirAll (workspaceDir , 0755 ); err != nil {
539+ t .Fatalf ("failed to create workspace dir: %v" , err )
540+ }
541+ if err := os .MkdirAll (outsideDir , 0755 ); err != nil {
542+ t .Fatalf ("failed to create outside dir: %v" , err )
543+ }
544+
545+ // Create target file outside workspace
546+ if err := os .WriteFile (secretFile , []byte ("secret data" ), 0644 ); err != nil {
547+ t .Fatalf ("failed to create secret file: %v" , err )
548+ }
549+
550+ // Create symlink using relative path with parent traversal: ../outside/secret.txt
551+ maliciousLink := filepath .Join (workspaceDir , "malicious" )
552+ if err := os .Symlink ("../outside/secret.txt" , maliciousLink ); err != nil {
553+ t .Skipf ("symlink not supported: %v" , err )
554+ }
555+
556+ // This should be rejected - symlink uses relative traversal to escape
557+ err := ValidatePathWithSymlinkResolution ("malicious" , workspaceDir )
558+ if err == nil {
559+ t .Error ("ValidatePathWithSymlinkResolution() should reject relative symlink traversal" )
560+ }
561+ })
562+
563+ t .Run ("absolute symlink to system path should be rejected" , func (t * testing.T ) {
564+ tmpDir := t .TempDir ()
565+ workspaceDir := filepath .Join (tmpDir , "workspace" )
566+
567+ if err := os .MkdirAll (workspaceDir , 0755 ); err != nil {
568+ t .Fatalf ("failed to create workspace dir: %v" , err )
569+ }
570+
571+ // Create symlink pointing to absolute system path
572+ absLink := filepath .Join (workspaceDir , "etc-link" )
573+ if err := os .Symlink ("/etc" , absLink ); err != nil {
574+ t .Skipf ("symlink not supported: %v" , err )
575+ }
576+
577+ // This should be rejected - absolute symlink to system directory
578+ err := ValidatePathWithSymlinkResolution ("etc-link" , workspaceDir )
579+ if err == nil {
580+ t .Error ("ValidatePathWithSymlinkResolution() should reject absolute symlink to system path" )
581+ }
582+ })
583+
584+ t .Run ("nested symlink in subdirectory escaping workspace should be rejected" , func (t * testing.T ) {
585+ tmpDir := t .TempDir ()
586+ workspaceDir := filepath .Join (tmpDir , "workspace" )
587+ subDir := filepath .Join (workspaceDir , "subdir" , "deep" , "nested" )
588+ outsideDir := filepath .Join (tmpDir , "outside" )
589+ secretFile := filepath .Join (outsideDir , "secret.txt" )
590+
591+ // Create directory structure
592+ if err := os .MkdirAll (subDir , 0755 ); err != nil {
593+ t .Fatalf ("failed to create subdirs: %v" , err )
594+ }
595+ if err := os .MkdirAll (outsideDir , 0755 ); err != nil {
596+ t .Fatalf ("failed to create outside dir: %v" , err )
597+ }
598+
599+ // Create target file
600+ if err := os .WriteFile (secretFile , []byte ("secret" ), 0644 ); err != nil {
601+ t .Fatalf ("failed to create secret file: %v" , err )
602+ }
603+
604+ // Create symlink deep in directory tree that escapes using multiple ../
605+ deepLink := filepath .Join (subDir , "escape" )
606+ if err := os .Symlink ("../../../../outside/secret.txt" , deepLink ); err != nil {
607+ t .Skipf ("symlink not supported: %v" , err )
608+ }
609+
610+ // This should be rejected
611+ err := ValidatePathWithSymlinkResolution ("subdir/deep/nested/escape" , workspaceDir )
612+ if err == nil {
613+ t .Error ("ValidatePathWithSymlinkResolution() should reject nested symlink that escapes workspace" )
614+ }
615+ })
616+
617+ t .Run ("symlink directory traversal via intermediate path should be rejected" , func (t * testing.T ) {
618+ tmpDir := t .TempDir ()
619+ workspaceDir := filepath .Join (tmpDir , "workspace" )
620+ outsideDir := filepath .Join (tmpDir , "outside" )
621+ targetDir := filepath .Join (outsideDir , "target" )
622+ secretFile := filepath .Join (targetDir , "secret.txt" )
623+
624+ // Create directory structure
625+ if err := os .MkdirAll (workspaceDir , 0755 ); err != nil {
626+ t .Fatalf ("failed to create workspace dir: %v" , err )
627+ }
628+ if err := os .MkdirAll (targetDir , 0755 ); err != nil {
629+ t .Fatalf ("failed to create target dir: %v" , err )
630+ }
631+
632+ // Create target file
633+ if err := os .WriteFile (secretFile , []byte ("secret" ), 0644 ); err != nil {
634+ t .Fatalf ("failed to create secret file: %v" , err )
635+ }
636+
637+ // Create a directory symlink that points outside
638+ evilDir := filepath .Join (workspaceDir , "evil-dir" )
639+ if err := os .Symlink (outsideDir , evilDir ); err != nil {
640+ t .Skipf ("symlink not supported: %v" , err )
641+ }
642+
643+ // Access file through symlinked directory: evil-dir/target/secret.txt
644+ // The path "evil-dir/target/secret.txt" looks safe but evil-dir points outside
645+ err := ValidatePathWithSymlinkResolution ("evil-dir/target/secret.txt" , workspaceDir )
646+ if err == nil {
647+ t .Error ("ValidatePathWithSymlinkResolution() should reject path through symlink directory pointing outside" )
648+ }
649+ })
650+
651+ t .Run ("symlink to parent directory should be rejected" , func (t * testing.T ) {
652+ tmpDir := t .TempDir ()
653+ workspaceDir := filepath .Join (tmpDir , "workspace" )
654+
655+ if err := os .MkdirAll (workspaceDir , 0755 ); err != nil {
656+ t .Fatalf ("failed to create workspace dir: %v" , err )
657+ }
658+
659+ // Create symlink pointing to parent directory
660+ parentLink := filepath .Join (workspaceDir , "parent" )
661+ if err := os .Symlink (".." , parentLink ); err != nil {
662+ t .Skipf ("symlink not supported: %v" , err )
663+ }
664+
665+ // Symlink resolves to tmpDir which is outside workspace
666+ err := ValidatePathWithSymlinkResolution ("parent" , workspaceDir )
667+ if err == nil {
668+ t .Error ("ValidatePathWithSymlinkResolution() should reject symlink to parent directory" )
669+ }
670+ })
671+
672+ t .Run ("triple chained symlinks escaping workspace should be rejected" , func (t * testing.T ) {
673+ tmpDir := t .TempDir ()
674+ workspaceDir := filepath .Join (tmpDir , "workspace" )
675+ outsideDir := filepath .Join (tmpDir , "outside" )
676+ secretFile := filepath .Join (outsideDir , "secret.txt" )
677+
678+ if err := os .MkdirAll (workspaceDir , 0755 ); err != nil {
679+ t .Fatalf ("failed to create workspace dir: %v" , err )
680+ }
681+ if err := os .MkdirAll (outsideDir , 0755 ); err != nil {
682+ t .Fatalf ("failed to create outside dir: %v" , err )
683+ }
684+ if err := os .WriteFile (secretFile , []byte ("secret" ), 0644 ); err != nil {
685+ t .Fatalf ("failed to create secret file: %v" , err )
686+ }
687+
688+ // Create triple chained symlinks: start -> middle -> end -> outside
689+ endLink := filepath .Join (workspaceDir , "end" )
690+ middleLink := filepath .Join (workspaceDir , "middle" )
691+ startLink := filepath .Join (workspaceDir , "start" )
692+
693+ if err := os .Symlink (secretFile , endLink ); err != nil {
694+ t .Skipf ("symlink not supported: %v" , err )
695+ }
696+ if err := os .Symlink ("end" , middleLink ); err != nil {
697+ t .Skipf ("symlink not supported: %v" , err )
698+ }
699+ if err := os .Symlink ("middle" , startLink ); err != nil {
700+ t .Skipf ("symlink not supported: %v" , err )
701+ }
702+
703+ err := ValidatePathWithSymlinkResolution ("start" , workspaceDir )
704+ if err == nil {
705+ t .Error ("ValidatePathWithSymlinkResolution() should reject triple chained symlinks" )
706+ }
707+ })
708+
709+ t .Run ("symlink with dot-dot in target should be rejected" , func (t * testing.T ) {
710+ tmpDir := t .TempDir ()
711+ workspaceDir := filepath .Join (tmpDir , "workspace" )
712+ subDir := filepath .Join (workspaceDir , "subdir" )
713+ outsideFile := filepath .Join (tmpDir , "outside.txt" )
714+
715+ if err := os .MkdirAll (subDir , 0755 ); err != nil {
716+ t .Fatalf ("failed to create subdir: %v" , err )
717+ }
718+ if err := os .WriteFile (outsideFile , []byte ("outside" ), 0644 ); err != nil {
719+ t .Fatalf ("failed to create outside file: %v" , err )
720+ }
721+
722+ // Create symlink: workspace/subdir/link -> ../../outside.txt
723+ dotDotLink := filepath .Join (subDir , "link" )
724+ if err := os .Symlink ("../../outside.txt" , dotDotLink ); err != nil {
725+ t .Skipf ("symlink not supported: %v" , err )
726+ }
727+
728+ err := ValidatePathWithSymlinkResolution ("subdir/link" , workspaceDir )
729+ if err == nil {
730+ t .Error ("ValidatePathWithSymlinkResolution() should reject symlink with .. in target" )
731+ }
732+ })
733+
734+ t .Run ("symlink loop should return error" , func (t * testing.T ) {
735+ tmpDir := t .TempDir ()
736+ workspaceDir := filepath .Join (tmpDir , "workspace" )
737+
738+ if err := os .MkdirAll (workspaceDir , 0755 ); err != nil {
739+ t .Fatalf ("failed to create workspace dir: %v" , err )
740+ }
741+
742+ // Create circular symlinks: loopA -> loopB -> loopA
743+ loopA := filepath .Join (workspaceDir , "loopA" )
744+ loopB := filepath .Join (workspaceDir , "loopB" )
745+
746+ if err := os .Symlink ("loopB" , loopA ); err != nil {
747+ t .Skipf ("symlink not supported: %v" , err )
748+ }
749+ if err := os .Symlink ("loopA" , loopB ); err != nil {
750+ t .Skipf ("symlink not supported: %v" , err )
751+ }
752+
753+ // EvalSymlinks should fail on circular symlinks
754+ err := ValidatePathWithSymlinkResolution ("loopA" , workspaceDir )
755+ if err == nil {
756+ t .Error ("ValidatePathWithSymlinkResolution() should return error for symlink loop" )
757+ }
758+ })
759+
760+ t .Run ("mixed path traversal and symlink attack should be rejected" , func (t * testing.T ) {
761+ tmpDir := t .TempDir ()
762+ workspaceDir := filepath .Join (tmpDir , "workspace" )
763+ subDir := filepath .Join (workspaceDir , "sub" )
764+ outsideDir := filepath .Join (tmpDir , "outside" )
765+
766+ if err := os .MkdirAll (subDir , 0755 ); err != nil {
767+ t .Fatalf ("failed to create subdir: %v" , err )
768+ }
769+ if err := os .MkdirAll (outsideDir , 0755 ); err != nil {
770+ t .Fatalf ("failed to create outside dir: %v" , err )
771+ }
772+
773+ // Create symlink that leads outside
774+ linkInSub := filepath .Join (subDir , "escape" )
775+ if err := os .Symlink (outsideDir , linkInSub ); err != nil {
776+ t .Skipf ("symlink not supported: %v" , err )
777+ }
778+
779+ // Path: sub/escape which resolves to outside directory
780+ err := ValidatePathWithSymlinkResolution ("sub/escape" , workspaceDir )
781+ if err == nil {
782+ t .Error ("ValidatePathWithSymlinkResolution() should reject symlink escape in subdirectory" )
783+ }
784+ })
785+
786+ t .Run ("symlink with encoded traversal patterns should be rejected" , func (t * testing.T ) {
787+ tmpDir := t .TempDir ()
788+ workspaceDir := filepath .Join (tmpDir , "workspace" )
789+ outsideDir := filepath .Join (tmpDir , "outside" )
790+ secretFile := filepath .Join (outsideDir , "secret" )
791+
792+ if err := os .MkdirAll (workspaceDir , 0755 ); err != nil {
793+ t .Fatalf ("failed to create workspace dir: %v" , err )
794+ }
795+ if err := os .MkdirAll (outsideDir , 0755 ); err != nil {
796+ t .Fatalf ("failed to create outside dir: %v" , err )
797+ }
798+ if err := os .WriteFile (secretFile , []byte ("secret" ), 0644 ); err != nil {
799+ t .Fatalf ("failed to create secret file: %v" , err )
800+ }
801+
802+ // Symlink with path that after resolution goes outside
803+ trickLink := filepath .Join (workspaceDir , "innocent.txt" )
804+ if err := os .Symlink ("../outside/secret" , trickLink ); err != nil {
805+ t .Skipf ("symlink not supported: %v" , err )
806+ }
807+
808+ err := ValidatePathWithSymlinkResolution ("innocent.txt" , workspaceDir )
809+ if err == nil {
810+ t .Error ("ValidatePathWithSymlinkResolution() should reject disguised symlink traversal" )
811+ }
812+ })
813+
814+ t .Run ("deeply nested chained symlinks should be rejected" , func (t * testing.T ) {
815+ tmpDir := t .TempDir ()
816+ workspaceDir := filepath .Join (tmpDir , "workspace" )
817+ outsideFile := filepath .Join (tmpDir , "secret.txt" )
818+
819+ if err := os .MkdirAll (workspaceDir , 0755 ); err != nil {
820+ t .Fatalf ("failed to create workspace dir: %v" , err )
821+ }
822+ if err := os .WriteFile (outsideFile , []byte ("secret" ), 0644 ); err != nil {
823+ t .Fatalf ("failed to create outside file: %v" , err )
824+ }
825+
826+ // Create 5 levels of symlink chain, final one pointing outside
827+ links := make ([]string , 5 )
828+ for i := 0 ; i < 5 ; i ++ {
829+ links [i ] = filepath .Join (workspaceDir , fmt .Sprintf ("link%d" , i ))
830+ }
831+
832+ // Last link points outside
833+ if err := os .Symlink (outsideFile , links [4 ]); err != nil {
834+ t .Skipf ("symlink not supported: %v" , err )
835+ }
836+ // Each previous link points to the next
837+ for i := 3 ; i >= 0 ; i -- {
838+ if err := os .Symlink (fmt .Sprintf ("link%d" , i + 1 ), links [i ]); err != nil {
839+ t .Skipf ("symlink not supported: %v" , err )
840+ }
841+ }
842+
843+ err := ValidatePathWithSymlinkResolution ("link0" , workspaceDir )
844+ if err == nil {
845+ t .Error ("ValidatePathWithSymlinkResolution() should reject deeply nested symlink chain" )
846+ }
847+ })
848+
849+ t .Run ("symlink targeting tmp directory should be rejected" , func (t * testing.T ) {
850+ tmpDir := t .TempDir ()
851+ workspaceDir := filepath .Join (tmpDir , "workspace" )
852+
853+ if err := os .MkdirAll (workspaceDir , 0755 ); err != nil {
854+ t .Fatalf ("failed to create workspace dir: %v" , err )
855+ }
856+
857+ // Create symlink to /tmp (a common attack target)
858+ tmpLink := filepath .Join (workspaceDir , "tmp-link" )
859+ if err := os .Symlink ("/tmp" , tmpLink ); err != nil {
860+ t .Skipf ("symlink not supported: %v" , err )
861+ }
862+
863+ err := ValidatePathWithSymlinkResolution ("tmp-link" , workspaceDir )
864+ if err == nil {
865+ t .Error ("ValidatePathWithSymlinkResolution() should reject symlink to /tmp" )
866+ }
867+ })
868+ }
0 commit comments