You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2019/09/09 09:54:31 UTC

[mina-sshd] branch master updated: [SSHD-926] Add support for OpenSSH 'lsetstat@openssh.com' SFTP protocol extension

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 'lsetstat@openssh.com' SFTP protocol extension
de2e460 is described below

commit de2e460098a98f8cb8fd2414794971e3ba8a6b0c
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Wed Aug 28 15:22:58 2019 +0300

    [SSHD-926] Add support for OpenSSH 'lsetstat@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 'lsetstat@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 `lsetstat@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
 * `hardlink@openssh.com`
 * `posix-rename@openssh.com`
 * `statvfs@openssh.com`
+* `lsetstat@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:dev@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 = "fsync@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:dev@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 = "fsync@openssh.com";
-    public static final FsyncExtensionParser INSTANCE = new FsyncExtensionParser();
+public class LSetStatExtensionParser extends AbstractOpenSSHExtensionParser {
+    public static final String NAME = "lsetstat@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