This is an automated email from the ASF dual-hosted git repository. lgoldstein pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
The following commit(s) were added to refs/heads/master by this push: new de2e460 [SSHD-926] Add support for OpenSSH 'lsets...@openssh.com' SFTP protocol extension de2e460 is described below commit de2e460098a98f8cb8fd2414794971e3ba8a6b0c Author: Lyor Goldstein <lgoldst...@apache.org> AuthorDate: Wed Aug 28 15:22:58 2019 +0300 [SSHD-926] Add support for OpenSSH 'lsets...@openssh.com' SFTP protocol extension --- CHANGES.md | 7 +- docs/sftp.md | 11 +++ .../subsystem/sftp/extensions/ParserUtils.java | 4 +- .../openssh/AbstractOpenSSHExtensionParser.java | 2 +- .../extensions/openssh/FsyncExtensionParser.java | 2 +- ...ionParser.java => LSetStatExtensionParser.java} | 13 ++- .../sftp/AbstractSftpSubsystemHelper.java | 102 +++++++++++++-------- .../sshd/server/subsystem/sftp/FileHandle.java | 12 ++- .../sshd/server/subsystem/sftp/SftpSubsystem.java | 5 +- 9 files changed, 106 insertions(+), 52 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 62f36db..7f77a16 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -22,6 +22,9 @@ session is initiated and protect their instance from shutdown when session is de `createSubsystem` method that accepts the `ChannelSession` through which the request has been made +* `AbstractSftpSubsystemHelper#resolvePathResolutionFollowLinks` is consulted wherever +the standard does not specifically specify the behavior regarding symbolic links handling. + * `UserAuthFactory` is a proper interface and it has been refactored to contain a `createUserAuth` method that accepts the session instance through which the request is made. @@ -35,6 +38,8 @@ peer version data is received. ## Behavioral changes and enhancements +* [SSHD-926](https://issues.apache.org/jira/browse/SSHD-930) - Add support for OpenSSH 'lsets...@openssh.com' SFTP protocol extension. + * [SSHD-930](https://issues.apache.org/jira/browse/SSHD-930) - Added configuration allowing the user to specify whether client should wait for the server's identification before sending its own. @@ -42,4 +47,4 @@ for the server's identification before sending its own. * [SSHD-934](https://issues.apache.org/jira/browse/SSHD-934) - Fixed ECDSA public key encoding into OpenSSH format. -* [SSHD-937](https://issues.apache.org/jira/browse/SSHD-937) - Provide session instance when creating a subsystem, user authentication, channel. \ No newline at end of file +* [SSHD-937](https://issues.apache.org/jira/browse/SSHD-937) - Provide session instance when creating a subsystem, user authentication, channel. diff --git a/docs/sftp.md b/docs/sftp.md index 919d761..ff017be 100644 --- a/docs/sftp.md +++ b/docs/sftp.md @@ -126,6 +126,16 @@ reasonable buffer size by setting the `channel-session-max-extdata-bufsize` prop extended data handler is registered it will be buffered (up to the specified max. size). **Note:** if a buffer size is configured but no extended data handler is registered when channel is spawning the command then an exception will occur. +### Symbolic links handling + +Whenever the server needs to execute a command that may behave differently if applied to a symbolic link instead of its target +it consults the `AbstractSftpSubsystemHelper#resolvePathResolutionFollowLinks` method. By default, this method simply consults +the value of the `sftp-auto-follow-links` configuration property (default=*true*). + +**Note:** the property is consulted only for cases where there is no clear indication in the standard how to behave for the +specific command. E.g., the `lsets...@openssh.com` specifically specifies that symbolic links should not be followed, so the +implementation does not consult the aforementioned property. + ## Client-side SFTP In order to obtain an `SftpClient` instance one needs to use an `SftpClientFactory`: @@ -424,6 +434,7 @@ Furthermore several [OpenSSH SFTP extensions](https://github.com/openssh/openssh * `hardl...@openssh.com` * `posix-ren...@openssh.com` * `stat...@openssh.com` +* `lsets...@openssh.com` On the server side, the reported standard extensions are configured via the `SftpSubsystem.CLIENT_EXTENSIONS_PROP` configuration key, and the _OpenSSH_ ones via the `SftpSubsystem.OPENSSH_EXTENSIONS_PROP`. diff --git a/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/ParserUtils.java b/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/ParserUtils.java index 9c4231a..119fe81 100644 --- a/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/ParserUtils.java +++ b/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/ParserUtils.java @@ -39,6 +39,7 @@ import org.apache.sshd.common.subsystem.sftp.extensions.SupportedParser.Supporte import org.apache.sshd.common.subsystem.sftp.extensions.openssh.FstatVfsExtensionParser; import org.apache.sshd.common.subsystem.sftp.extensions.openssh.FsyncExtensionParser; import org.apache.sshd.common.subsystem.sftp.extensions.openssh.HardLinkExtensionParser; +import org.apache.sshd.common.subsystem.sftp.extensions.openssh.LSetStatExtensionParser; import org.apache.sshd.common.subsystem.sftp.extensions.openssh.PosixRenameExtensionParser; import org.apache.sshd.common.subsystem.sftp.extensions.openssh.StatVfsExtensionParser; import org.apache.sshd.common.util.GenericUtils; @@ -62,7 +63,8 @@ public final class ParserUtils { StatVfsExtensionParser.INSTANCE, FstatVfsExtensionParser.INSTANCE, HardLinkExtensionParser.INSTANCE, - FsyncExtensionParser.INSTANCE + FsyncExtensionParser.INSTANCE, + LSetStatExtensionParser.INSTANCE )); private static final NavigableMap<String, ExtensionParser<?>> PARSERS_MAP = diff --git a/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/openssh/AbstractOpenSSHExtensionParser.java b/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/openssh/AbstractOpenSSHExtensionParser.java index 8590e64..9fb30fe 100644 --- a/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/openssh/AbstractOpenSSHExtensionParser.java +++ b/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/openssh/AbstractOpenSSHExtensionParser.java @@ -80,7 +80,7 @@ public abstract class AbstractOpenSSHExtensionParser extends AbstractParser<Open OpenSSHExtension other = (OpenSSHExtension) obj; return Objects.equals(getName(), other.getName()) - && Objects.equals(getVersion(), other.getVersion()); + && Objects.equals(getVersion(), other.getVersion()); } @Override diff --git a/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/openssh/FsyncExtensionParser.java b/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/openssh/FsyncExtensionParser.java index e9967ab..64c1249 100644 --- a/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/openssh/FsyncExtensionParser.java +++ b/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/openssh/FsyncExtensionParser.java @@ -21,7 +21,7 @@ package org.apache.sshd.common.subsystem.sftp.extensions.openssh; /** * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> - * @see <A HREF="https://github.com/openssh/openssh-portable/blob/master/PROTOCOL">OpenSSH - section 10</A> + * @see <A HREF="https://github.com/openssh/openssh-portable/blob/master/PROTOCOL">OpenSSH - section 10</A> */ public class FsyncExtensionParser extends AbstractOpenSSHExtensionParser { public static final String NAME = "fs...@openssh.com"; diff --git a/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/openssh/FsyncExtensionParser.java b/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/openssh/LSetStatExtensionParser.java similarity index 67% copy from sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/openssh/FsyncExtensionParser.java copy to sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/openssh/LSetStatExtensionParser.java index e9967ab..b60d63c 100644 --- a/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/openssh/FsyncExtensionParser.java +++ b/sshd-sftp/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/openssh/LSetStatExtensionParser.java @@ -20,14 +20,17 @@ package org.apache.sshd.common.subsystem.sftp.extensions.openssh; /** + * Replicates the functionality of the existing {@code SSH_FXP_SETSTAT} operation + * but does not follow symbolic links + * * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> - * @see <A HREF="https://github.com/openssh/openssh-portable/blob/master/PROTOCOL">OpenSSH - section 10</A> + * @see <A HREF="https://www.openssh.com/txt/release-8.0">OpenSSH v8.0 release notes</A> */ -public class FsyncExtensionParser extends AbstractOpenSSHExtensionParser { - public static final String NAME = "fs...@openssh.com"; - public static final FsyncExtensionParser INSTANCE = new FsyncExtensionParser(); +public class LSetStatExtensionParser extends AbstractOpenSSHExtensionParser { + public static final String NAME = "lsets...@openssh.com"; + public static final LSetStatExtensionParser INSTANCE = new LSetStatExtensionParser(); - public FsyncExtensionParser() { + public LSetStatExtensionParser() { super(NAME); } } diff --git a/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/AbstractSftpSubsystemHelper.java b/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/AbstractSftpSubsystemHelper.java index 3423398..d489e5f 100644 --- a/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/AbstractSftpSubsystemHelper.java +++ b/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/AbstractSftpSubsystemHelper.java @@ -89,6 +89,7 @@ import org.apache.sshd.common.subsystem.sftp.extensions.SpaceAvailableExtensionI import org.apache.sshd.common.subsystem.sftp.extensions.openssh.AbstractOpenSSHExtensionParser.OpenSSHExtension; import org.apache.sshd.common.subsystem.sftp.extensions.openssh.FsyncExtensionParser; import org.apache.sshd.common.subsystem.sftp.extensions.openssh.HardLinkExtensionParser; +import org.apache.sshd.common.subsystem.sftp.extensions.openssh.LSetStatExtensionParser; import org.apache.sshd.common.util.EventListenerUtils; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.MapEntryUtils.NavigableMapBuilder; @@ -161,7 +162,8 @@ public abstract class AbstractSftpSubsystemHelper Collections.unmodifiableList( Arrays.asList( new OpenSSHExtension(FsyncExtensionParser.NAME, "1"), - new OpenSSHExtension(HardLinkExtensionParser.NAME, "1") + new OpenSSHExtension(HardLinkExtensionParser.NAME, "1"), + new OpenSSHExtension(LSetStatExtensionParser.NAME, "1") )); public static final List<String> DEFAULT_OPEN_SSH_EXTENSIONS_NAMES = @@ -393,7 +395,7 @@ public abstract class AbstractSftpSubsystemHelper doFStat(buffer, id); break; case SftpConstants.SSH_FXP_SETSTAT: - doSetStat(buffer, id); + doSetStat(buffer, id, "", type, null); break; case SftpConstants.SSH_FXP_FSETSTAT: doFSetStat(buffer, id); @@ -660,11 +662,13 @@ public abstract class AbstractSftpSubsystemHelper return resolveFileAttributes(p, flags, IoUtils.getLinkOptions(false)); } - protected void doSetStat(Buffer buffer, int id) throws IOException { + protected void doSetStat( + Buffer buffer, int id, String extension, int cmd, Boolean followLinks /* null = auto-resolve */) + throws IOException { String path = buffer.getString(); Map<String, Object> attrs = readAttrs(buffer); try { - doSetStat(id, path, attrs); + doSetStat(id, path, cmd, extension, attrs, followLinks); } catch (IOException | RuntimeException e) { sendStatus(prepareReply(buffer), id, e, SftpConstants.SSH_FXP_SETSTAT, path); return; @@ -673,13 +677,19 @@ public abstract class AbstractSftpSubsystemHelper sendStatus(prepareReply(buffer), id, SftpConstants.SSH_FX_OK, ""); } - protected void doSetStat(int id, String path, Map<String, ?> attrs) throws IOException { + protected void doSetStat( + int id, String path, int cmd, String extension, Map<String, ?> attrs, Boolean followLinks /* null = auto-resolve */) + throws IOException { if (log.isDebugEnabled()) { - log.debug("doSetStat({})[id={}] SSH_FXP_SETSTAT (path={}, attrs={})", - getServerSession(), id, path, attrs); + log.debug("doSetStat({})[id={}, cmd={}, extension={}] (path={}, attrs={}, followLinks={})", + getServerSession(), id, cmd, extension, path, attrs, followLinks); } + Path p = resolveFile(path); - doSetAttributes(p, attrs); + if (followLinks == null) { + followLinks = resolvePathResolutionFollowLinks(cmd, extension, p); + } + doSetAttributes(p, attrs, followLinks); } protected void doFStat(Buffer buffer, int id) throws IOException { @@ -1579,7 +1589,9 @@ public abstract class AbstractSftpSubsystemHelper listener.creating(session, p, attrs); try { Files.createDirectory(p); - doSetAttributes(p, attrs); + boolean followLinks = resolvePathResolutionFollowLinks( + SftpConstants.SSH_FXP_MKDIR, "", p); + doSetAttributes(p, attrs, followLinks); } catch (IOException | RuntimeException e) { listener.created(session, p, attrs, e); throw e; @@ -1682,9 +1694,11 @@ public abstract class AbstractSftpSubsystemHelper case HardLinkExtensionParser.NAME: doOpenSSHHardLink(buffer, id); break; + case LSetStatExtensionParser.NAME: + doSetStat(buffer, id, extension, -1, Boolean.FALSE); + break; default: doUnsupportedExtension(buffer, id, extension); - break; } } @@ -1697,35 +1711,38 @@ public abstract class AbstractSftpSubsystemHelper } protected void appendExtensions(Buffer buffer, String supportedVersions) { - appendVersionsExtension(buffer, supportedVersions); - appendNewlineExtension(buffer, resolveNewlineValue(getServerSession())); - appendVendorIdExtension(buffer, VersionProperties.getVersionProperties()); - appendOpenSSHExtensions(buffer); - appendAclSupportedExtension(buffer); + ServerSession session = getServerSession(); + appendVersionsExtension(buffer, supportedVersions, session); + appendNewlineExtension(buffer, session); + appendVendorIdExtension(buffer, VersionProperties.getVersionProperties(), session); + appendOpenSSHExtensions(buffer, session); + appendAclSupportedExtension(buffer, session); - Map<String, OptionalFeature> extensions = getSupportedClientExtensions(); + Map<String, OptionalFeature> extensions = getSupportedClientExtensions(session); int numExtensions = GenericUtils.size(extensions); - List<String> extras = (numExtensions <= 0) ? Collections.emptyList() : new ArrayList<>(numExtensions); + List<String> extras = + (numExtensions <= 0) ? Collections.emptyList() : new ArrayList<>(numExtensions); if (numExtensions > 0) { - ServerSession session = getServerSession(); boolean debugEnabled = log.isDebugEnabled(); - extensions.forEach((name, f) -> { + for (Map.Entry<String, OptionalFeature> ee : extensions.entrySet()) { + String name = ee.getKey(); + OptionalFeature f = ee.getValue(); if (!f.isSupported()) { if (debugEnabled) { log.debug("appendExtensions({}) skip unsupported extension={}", session, name); } - return; + continue; } extras.add(name); - }); + } } + appendSupportedExtension(buffer, extras); appendSupported2Extension(buffer, extras); } - protected int appendAclSupportedExtension(Buffer buffer) { - ServerSession session = getServerSession(); + protected int appendAclSupportedExtension(Buffer buffer, ServerSession session) { Collection<Integer> maskValues = resolveAclSupportedCapabilities(session); int mask = AclSupportedParser.AclCapabilities.constructAclCapabilities(maskValues); if (mask != 0) { @@ -1772,8 +1789,8 @@ public abstract class AbstractSftpSubsystemHelper return maskValues; } - protected List<OpenSSHExtension> appendOpenSSHExtensions(Buffer buffer) { - List<OpenSSHExtension> extList = resolveOpenSSHExtensions(getServerSession()); + protected List<OpenSSHExtension> appendOpenSSHExtensions(Buffer buffer, ServerSession session) { + List<OpenSSHExtension> extList = resolveOpenSSHExtensions(session); if (GenericUtils.isEmpty(extList)) { return extList; } @@ -1820,8 +1837,7 @@ public abstract class AbstractSftpSubsystemHelper return extList; } - protected Map<String, OptionalFeature> getSupportedClientExtensions() { - ServerSession session = getServerSession(); + protected Map<String, OptionalFeature> getSupportedClientExtensions(ServerSession session) { String value = session.getString(CLIENT_EXTENSIONS_PROP); if (value == null) { return DEFAULT_SUPPORTED_CLIENT_EXTENSIONS; @@ -1855,15 +1871,16 @@ public abstract class AbstractSftpSubsystemHelper * * @param buffer The {@link Buffer} to append to * @param value The recommended value - ignored if {@code null}/empty + * @param session The {@link ServerSession} for which this extension is added * @see SftpConstants#EXT_VERSIONS */ - protected void appendVersionsExtension(Buffer buffer, String value) { + protected void appendVersionsExtension(Buffer buffer, String value, ServerSession session) { if (GenericUtils.isEmpty(value)) { return; } if (log.isDebugEnabled()) { - log.debug("appendVersionsExtension({}) value={}", getServerSession(), value); + log.debug("appendVersionsExtension({}) value={}", session, value); } buffer.putString(SftpConstants.EXT_VERSIONS); @@ -1876,10 +1893,12 @@ public abstract class AbstractSftpSubsystemHelper * or use the correct extension name * * @param buffer The {@link Buffer} to append to - * @param value The recommended value - ignored if {@code null}/empty + * @param session The {@link ServerSession} for which this extension is added * @see SftpConstants#EXT_NEWLINE + * @see #resolveNewlineValue(ServerSession) */ - protected void appendNewlineExtension(Buffer buffer, String value) { + protected void appendNewlineExtension(Buffer buffer, ServerSession session) { + String value = resolveNewlineValue(session); if (GenericUtils.isEmpty(value)) { return; } @@ -1915,20 +1934,22 @@ public abstract class AbstractSftpSubsystemHelper * <LI>{@code artifactId} - as the product name</LI> * <LI>{@code version} - as the product version</LI> * </UL> + * @param session The {@link ServerSession} for which these properties are added * @see SftpConstants#EXT_VENDOR_ID * @see <A HREF="http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt">DRAFT 09 - section 4.4</A> */ - protected void appendVendorIdExtension(Buffer buffer, Map<String, ?> versionProperties) { + protected void appendVendorIdExtension(Buffer buffer, Map<String, ?> versionProperties, ServerSession session) { if (GenericUtils.isEmpty(versionProperties)) { return; } if (log.isDebugEnabled()) { - log.debug("appendVendorIdExtension({}): {}", getServerSession(), versionProperties); + log.debug("appendVendorIdExtension({}): {}", session, versionProperties); } buffer.putString(SftpConstants.EXT_VENDOR_ID); - PropertyResolver resolver = PropertyResolverUtils.toPropertyResolver(Collections.unmodifiableMap(versionProperties)); + PropertyResolver resolver = + PropertyResolverUtils.toPropertyResolver(Collections.unmodifiableMap(versionProperties)); // placeholder for length int lenPos = buffer.wpos(); buffer.putInt(0); @@ -2409,12 +2430,12 @@ public abstract class AbstractSftpSubsystemHelper return Collections.emptyNavigableMap(); } - protected void doSetAttributes(Path file, Map<String, ?> attributes) throws IOException { + protected void doSetAttributes(Path file, Map<String, ?> attributes, boolean followLinks) throws IOException { SftpEventListener listener = getSftpEventListenerProxy(); ServerSession session = getServerSession(); listener.modifyingAttributes(session, file, attributes); try { - setFileAttributes(file, attributes, IoUtils.getLinkOptions(false)); + setFileAttributes(file, attributes, IoUtils.getLinkOptions(followLinks)); } catch (IOException | RuntimeException e) { listener.modifiedAttributes(session, file, attributes, e); throw e; @@ -2423,11 +2444,16 @@ public abstract class AbstractSftpSubsystemHelper } protected LinkOption[] getPathResolutionLinkOption(int cmd, String extension, Path path) throws IOException { - ServerSession session = getServerSession(); - boolean followLinks = PropertyResolverUtils.getBooleanProperty(session, AUTO_FOLLOW_LINKS, DEFAULT_AUTO_FOLLOW_LINKS); + boolean followLinks = resolvePathResolutionFollowLinks(cmd, extension, path); return IoUtils.getLinkOptions(followLinks); } + protected boolean resolvePathResolutionFollowLinks(int cmd, String extension, Path path) throws IOException { + ServerSession session = getServerSession(); + return PropertyResolverUtils.getBooleanProperty( + session, AUTO_FOLLOW_LINKS, DEFAULT_AUTO_FOLLOW_LINKS); + } + protected void setFileAttributes(Path file, Map<String, ?> attributes, LinkOption... options) throws IOException { Set<String> unsupported = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); // Cannot use forEach because of the potential IOException being thrown diff --git a/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/FileHandle.java b/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/FileHandle.java index 19bd602..d14cf86 100644 --- a/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/FileHandle.java +++ b/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/FileHandle.java @@ -51,7 +51,9 @@ public class FileHandle extends Handle { private final Set<StandardOpenOption> openOptions; private final Collection<FileAttribute<?>> fileAttributes; - public FileHandle(SftpSubsystem subsystem, Path file, String handle, int flags, int access, Map<String, Object> attrs) throws IOException { + public FileHandle( + SftpSubsystem subsystem, Path file, String handle, int flags, int access, Map<String, Object> attrs) + throws IOException { super(subsystem, file, handle); this.access = access; @@ -67,10 +69,12 @@ public class FileHandle extends Handle { ServerSession session = subsystem.getServerSession(); SeekableByteChannel channel; try { - channel = accessor.openFile(session, subsystem, this, file, handle, openOptions, fileAttrs); + channel = accessor.openFile( + session, subsystem, this, file, handle, openOptions, fileAttrs); } catch (UnsupportedOperationException e) { - channel = accessor.openFile(session, subsystem, this, file, handle, openOptions, IoUtils.EMPTY_FILE_ATTRIBUTES); - subsystem.doSetAttributes(file, attrs); + channel = accessor.openFile( + session, subsystem, this, file, handle, openOptions, IoUtils.EMPTY_FILE_ATTRIBUTES); + subsystem.doSetAttributes(file, attrs, false); } this.fileChannel = channel; diff --git a/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java b/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java index 92a2b29..29f0c9f 100644 --- a/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java +++ b/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java @@ -767,7 +767,10 @@ public class SftpSubsystem } Handle fileHandle = validateHandle(handle, h, Handle.class); - doSetAttributes(fileHandle.getFile(), attrs); + Path path = fileHandle.getFile(); + boolean followLinks = resolvePathResolutionFollowLinks( + SftpConstants.SSH_FXP_FSETSTAT, "", path); + doSetAttributes(fileHandle.getFile(), attrs, followLinks); } @Override