Skip to content

Commit 707b989

Browse files
author
PortaSFTPServer
committed
Fix symlink containment by converting virtual paths to real Windows paths via RealUserHomes dictionary
1 parent 52bb785 commit 707b989

2 files changed

Lines changed: 75 additions & 1 deletion

File tree

ApacheMinaSSHD.NET.Wrapper/Internals/InternalSftpFileSystemAccessor.cs

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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();

ApacheMinaSSHD.NET.Wrapper/Internals/InternalVirtualFileSystemFactory.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,32 @@
33
using org.apache.sshd.common.file;
44
using org.apache.sshd.common.file.virtualfs;
55
using org.apache.sshd.common.session;
6+
using System.Collections.Concurrent;
67

78
namespace ApacheMinaSSHD.NET.Wrapper.Internals
89
{
910
internal sealed class InternalVirtualFileSystemFactory : java.lang.Object, FileSystemFactory
1011
{
1112
private readonly AMNetVirtualFileSystemFactory fileSystemFactory;
1213

14+
/// <summary>
15+
/// Maps authenticated usernames to their real (Windows) home directory paths.
16+
/// Used by <see cref="InternalSftpFileSystemAccessor"/> to convert virtual paths to
17+
/// real filesystem paths for symlink containment validation.
18+
/// </summary>
19+
internal static readonly ConcurrentDictionary<string, string> RealUserHomes = new();
20+
1321
public InternalVirtualFileSystemFactory(AMNetVirtualFileSystemFactory fileSystemFactory)
1422
{
1523
this.fileSystemFactory = fileSystemFactory;
1624
}
1725

1826
java.nio.file.FileSystem FileSystemFactory.createFileSystem(SessionContext sessionContext)
1927
{
28+
// Store the real user home path so that symlink detection can use real Windows paths.
29+
string userHome = fileSystemFactory.ResolveUserHomeDirectory(sessionContext.getUsername());
30+
RealUserHomes[sessionContext.getUsername()] = userHome;
31+
2032
return CreateVirtualFileSystemFactory(sessionContext).createFileSystem(sessionContext);
2133
}
2234

0 commit comments

Comments
 (0)