apache / mina-sshd

Apache MINA sshd is a comprehensive Java library for client- and server-side SSH.
https://mina.apache.org/sshd-project/
Apache License 2.0
879 stars 355 forks source link

Restricting User Access to Specific Directories in Apache SSHD SFTP Server #534

Open naujoks-stefan opened 1 month ago

naujoks-stefan commented 1 month ago

Version

1.7.0

Bug description

Hello everyone,

I'm working on a project where I need to set up an SFTP server using Apache SSHD, which allows users to access multiple directories within a specified root directory. The root directory should be restricted to C:/FTP, and users should only be able to see and access their designated subdirectories within this root directory.

Despite setting the C:/FTP as the root directory and specifying the user directories, users are seeing the entire C:/ drive instead of being restricted to C:/FTP and their specific directories.

Actual behavior

Current Setup I have implemented a custom SFTP server using Apache SSHD, and it successfully authenticates users. However, I'm facing issues with restricting the user's view to their specific directories. Instead of seeing their designated directories within C:/FTP, users can see the entire C:/ drive.

Here's the code I am using:

public class MySftpServer {

private static final Log log = LogFactory.getLog(MySftpServer.class);
private static final AttributeKey<UserManager.User> USER_KEY = new AttributeKey<>();

@PostConstruct
public void startServer() throws IOException {
    start();
}

private void start() throws IOException {
    SshServer sshd = SshServer.setUpDefaultServer();
    sshd.setHost("localhost");
    sshd.setPort(2222);
    sshd.setKeyPairProvider(new PEMKeyPairProvider("src/main/resources/host_key.ser"));
    sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
    sshd.setPasswordAuthenticator(new PasswordAuthenticator() {
        @Override
        public boolean authenticate(String username, String password, ServerSession session) {
            UserManager.User user = UserManager.authenticate(username, password);
            if (user != null) {
                session.setAttribute(USER_KEY, user);
                return true;
            }
            return false;
        }
    });
    sshd.setPublickeyAuthenticator(new AuthorizedKeysAuthenticator(Paths.get("src/main/resources/authorized_keys")));

    // Set idle timeout to 30 minutes
    sshd.getProperties().put(SshServer.IDLE_TIMEOUT, TimeUnit.MINUTES.toMillis(30));
    // Set authentication timeout to 2 minutes
    sshd.getProperties().put(SshServer.AUTH_TIMEOUT, TimeUnit.MINUTES.toMillis(2));

    // Set up the custom file system factory with user home directories
    sshd.setFileSystemFactory(new CustomFileSystemFactory(Paths.get("C:/FTP")));

    sshd.start();
    log.info("SFTP server started");
}

public static class CustomFileSystemFactory implements FileSystemFactory {
    private final Path rootDir;

    public CustomFileSystemFactory(Path rootDir) {
        this.rootDir = rootDir;
    }

    @Override
    public FileSystem createFileSystem(Session session) throws IOException {
        UserManager.User user = session.getAttribute(USER_KEY);
        if (user == null) {
            throw new IOException("No user found in session");
        }

        List<Path> userDirs = user.getDirectories().stream()
            .map(dir -> rootDir.resolve(dir).toAbsolutePath().normalize())
            .collect(Collectors.toList());
        return new MultiDirectoryFileSystem(userDirs);
    }
}

public static class MultiDirectoryFileSystem extends FileSystem {
    private final List<Path> roots;

    public MultiDirectoryFileSystem(List<Path> directories) {
        this.roots = directories;
    }

    @Override
    public FileSystemProvider provider() {
        return new MultiDirectoryFileSystemProvider(roots);
    }

    @Override
    public void close() throws IOException {
        // Implement close logic if needed
    }

    @Override
    public boolean isOpen() {
        return true;
    }

    @Override
    public boolean isReadOnly() {
        return false;
    }

    @Override
    public String getSeparator() {
        return FileSystems.getDefault().getSeparator();
    }

    @Override
    public Iterable<Path> getRootDirectories() {
        return roots;
    }

    @Override
    public Iterable<FileStore> getFileStores() {
        return Collections.emptyList();
    }

    @Override
    public Set<String> supportedFileAttributeViews() {
        return FileSystems.getDefault().supportedFileAttributeViews();
    }

    @Override
    public Path getPath(String first, String... more) {
        Path path = FileSystems.getDefault().getPath(first, more).toAbsolutePath().normalize();
        for (Path root : roots) {
            if (path.startsWith(root)) {
                return path;
            }
        }
        throw new IllegalArgumentException("Path is not under any root: " + path);
    }

    @Override
    public PathMatcher getPathMatcher(String syntaxAndPattern) {
        return FileSystems.getDefault().getPathMatcher(syntaxAndPattern);
    }

    @Override
    public UserPrincipalLookupService getUserPrincipalLookupService() {
        return FileSystems.getDefault().getUserPrincipalLookupService();
    }

    @Override
    public WatchService newWatchService() throws IOException {
        return FileSystems.getDefault().newWatchService();
    }
}

public static class MultiDirectoryFileSystemProvider extends FileSystemProvider {
    private final List<Path> roots;

    public MultiDirectoryFileSystemProvider(List<Path> roots) {
        this.roots = roots;
    }

    @Override
    public String getScheme() {
        return "multi-dir";
    }

    @Override
    public FileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
        throw new UnsupportedOperationException("Creating new file systems is not supported.");
    }

    @Override
    public FileSystem getFileSystem(URI uri) {
        throw new UnsupportedOperationException("Getting file system by URI is not supported.");
    }

    @Override
    public Path getPath(URI uri) {
        return Paths.get(uri).toAbsolutePath().normalize();
    }

    @Override
    public FileChannel newFileChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
        validateAccess(path);
        return FileSystems.getDefault().provider().newFileChannel(path, options, attrs);
    }

    @Override
    public AsynchronousFileChannel newAsynchronousFileChannel(Path path, Set<? extends OpenOption> options, ExecutorService executor, FileAttribute<?>... attrs) throws IOException {
        validateAccess(path);
        return FileSystems.getDefault().provider().newAsynchronousFileChannel(path, options, executor, attrs);
    }

    @Override
    public DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter) throws IOException {
        validateAccess(dir);
        return FileSystems.getDefault().provider().newDirectoryStream(dir, filter);
    }

    @Override
    public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException {
        validateAccess(dir);
        FileSystems.getDefault().provider().createDirectory(dir, attrs);
    }

    @Override
    public void delete(Path path) throws IOException {
        validateAccess(path);
        FileSystems.getDefault().provider().delete(path);
    }

    @Override
    public void copy(Path source, Path target, CopyOption... options) throws IOException {
        validateAccess(source);
        validateAccess(target);
        FileSystems.getDefault().provider().copy(source, target, options);
    }

    @Override
    public void move(Path source, Path target, CopyOption... options) throws IOException {
        validateAccess(source);
        validateAccess(target);
        FileSystems.getDefault().provider().move(source, target, options);
    }

    @Override
    public boolean isSameFile(Path path, Path path2) throws IOException {
        validateAccess(path);
        validateAccess(path2);
        return FileSystems.getDefault().provider().isSameFile(path, path2);
    }

    @Override
    public boolean isHidden(Path path) throws IOException {
        validateAccess(path);
        return FileSystems.getDefault().provider().isHidden(path);
    }

    @Override
    public FileStore getFileStore(Path path) throws IOException {
        validateAccess(path);
        return FileSystems.getDefault().provider().getFileStore(path);
    }

    @Override
    public void checkAccess(Path path, AccessMode... modes) throws IOException {
        validateAccess(path);
        FileSystems.getDefault().provider().checkAccess(path, modes);
    }

    @Override
    public <V extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, LinkOption... options) {
        try {
            validateAccess(path);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return FileSystems.getDefault().provider().getFileAttributeView(path, type, options);
    }

    @Override
    public <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options) throws IOException {
        validateAccess(path);
        return FileSystems.getDefault().provider().readAttributes(path, type, options);
    }

    @Override
    public Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options) throws IOException {
        validateAccess(path);
        return FileSystems.getDefault().provider().readAttributes(path, attributes, options);
    }

    @Override
    public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException {
        validateAccess(path);
        FileSystems.getDefault().provider().setAttribute(path, attribute, value, options);
    }

    private void validateAccess(Path path) throws IOException {
        for (Path root : roots) {
            if (path.startsWith(root)) {
                return;
            }
        }
        throw new IOException("Access denied to path: " + path);
    }

    @Override
    public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options,
                                              FileAttribute<?>... attrs) throws IOException {
        validateAccess(path);
        return FileSystems.getDefault().provider().newByteChannel(path, options, attrs);
    }
}

public static class PEMKeyPairProvider implements KeyPairProvider {

    private final KeyPair keyPair;

    public PEMKeyPairProvider(String privateKeyPath) throws IOException {
        try (PEMParser pemParser = new PEMParser(new FileReader(privateKeyPath))) {
            JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
            Object object = pemParser.readObject();
            if (object instanceof PEMKeyPair) {
                this.keyPair = converter.getKeyPair((PEMKeyPair) object);
            } else if (object instanceof PrivateKeyInfo) {
                PrivateKey privateKey = converter.getPrivateKey((PrivateKeyInfo) object);
                SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(pemParser.readObject());
                PublicKey publicKey = converter.getPublicKey(publicKeyInfo);
                this.keyPair = new KeyPair(publicKey, privateKey);
            } else {
                throw new IllegalArgumentException("Invalid key format");
            }
        }
    }

    @Override
    public Iterable<KeyPair> loadKeys() {
        return Collections.singletonList(keyPair);
    }
}

}

Expected behavior

The SFTP root directory should be C:/FTP. Users should only see and access their designated directories within C:/FTP. For example, user1 should only see C:/FTP/001 and C:/FTP/002.

Relevant log output

No response

Other information

No response

tomaswolf commented 1 month ago
  1. Version 1.7.0 is very old.
  2. We cannot help you debug your code. For instance: did you check that UserManager (seems to be your own code) returns the right user?