@@ -52,7 +52,10 @@ public Path resolveLocalFilePath(SftpSubsystemProxy subsystem, Path rootDir, str
5252 Path finalPath = ToPath ( managedPath , resolvedPath ) ;
5353 Console . WriteLine ( $ "[SRV] finalPath: { finalPath ? . toString ( ) } ") ;
5454
55- ValidateSymlinkContainment ( finalPath , rootDir ) ;
55+ // Use real Windows paths for symlink containment when available.
56+ // VirtualFileSystemView paths like "/outside-link.txt" are virtual and
57+ // cannot be used with File.ResolveLinkTarget, FindFirstFile, etc.
58+ ValidateSymlinkWithRealPath ( subsystem , finalPath , rootDir , remotePath ) ;
5659
5760 var validationContext = CreateContext (
5861 subsystem ,
@@ -519,6 +522,65 @@ public bool noFollow(Collection opts)
519522 return fileSystemAccessor . NoFollow ( context , defaultNoFollow ) ;
520523 }
521524
525+ private void ValidateSymlinkWithRealPath ( SftpSubsystemProxy subsystem , Path finalPath , Path rootDir , string remotePath )
526+ {
527+ Path effectivePath = finalPath ;
528+ Path effectiveRoot = rootDir ;
529+
530+ try
531+ {
532+ if ( InternalVirtualFileSystemFactory . RealUserHomes . TryGetValue (
533+ subsystem . getSession ( ) . getUsername ( ) , out string ? realUserHome ) )
534+ {
535+ string pathStr = effectivePath . toString ( ) ;
536+ // Convert virtual paths (Unix-style starting with '/') to real paths.
537+ // On Windows, real paths start with a drive letter so '/' always means virtual.
538+ // On Linux, both virtual and real paths start with '/', so we verify the path
539+ // is not already within the real root to avoid double-conversion.
540+ if ( pathStr . StartsWith ( '/' ) && IsVirtualNotRealPath ( pathStr , realUserHome ) )
541+ {
542+ string relativePath = pathStr . TrimStart ( '/' ) ;
543+ string realFilePath = System . IO . Path . GetFullPath (
544+ System . IO . Path . Combine ( realUserHome , relativePath ) ) ;
545+ string realRootPath = System . IO . Path . GetFullPath ( realUserHome ) ;
546+
547+ effectivePath = Paths . get ( realFilePath ) ;
548+ effectiveRoot = Paths . get ( realRootPath ) ;
549+ Console . WriteLine ( $ "[SRV] Real path for symlink check: { realFilePath } ") ;
550+ }
551+ }
552+ }
553+ catch ( Exception ex )
554+ {
555+ Console . WriteLine ( $ "[SRV-DIAG] Failed to resolve real path, using virtual: { ex . GetType ( ) . Name } : { ex . Message } ") ;
556+ }
557+
558+ ValidateSymlinkContainment ( effectivePath , effectiveRoot ) ;
559+ }
560+
561+ /// <summary>
562+ /// Determines whether a Unix-style path (starting with '/') is a virtual path
563+ /// rather than an already-resolved real path. On Windows, all paths starting with
564+ /// '/' are virtual. On Linux, we check that the path does not already fall within
565+ /// the real user home directory.
566+ /// </summary>
567+ private static bool IsVirtualNotRealPath ( string path , string realUserHome )
568+ {
569+ if ( OperatingSystem . IsWindows ( ) )
570+ {
571+ return true ;
572+ }
573+
574+ // On Linux, real paths also start with '/'. Check if this path is already
575+ // a real path by verifying it isn't the real root or a child of it.
576+ if ( path . Length <= realUserHome . Length )
577+ {
578+ return ! path . Equals ( realUserHome , StringComparison . Ordinal ) ;
579+ }
580+
581+ return ! path . StartsWith ( realUserHome + "/" , StringComparison . Ordinal ) ;
582+ }
583+
522584 private void ValidateSymlinkContainment ( Path filePath , Path rootDir )
523585 {
524586 string pathStr = filePath . toString ( ) ;
0 commit comments