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

commit 952bb0cf6a0dd82751ed79755c69335a6fb368fa
Author: Lyor Goldstein <lgoldst...@apache.org>
AuthorDate: Thu Oct 31 20:24:46 2019 +0200

    [SSHD-953] Parse and strip quoted command arguments when executing a 
server-side command via local shell
---
 CHANGES.md                                         |  1 +
 .../java/org/apache/sshd/common/util/OsUtils.java  | 16 +++-
 .../sshd/common/channel/ChannelOutputStream.java   | 31 ++++---
 .../apache/sshd/server/command/CommandFactory.java | 70 +++++++++++++++-
 .../shell/InteractiveProcessShellFactory.java      |  8 +-
 .../server/shell/ProcessShellCommandFactory.java   |  5 +-
 .../sshd/server/shell/ProcessShellFactory.java     | 52 ++++++++----
 .../server/command/CommandFactorySplitterTest.java | 97 ++++++++++++++++++++++
 8 files changed, 241 insertions(+), 39 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 9193bbf..761c61c 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -95,3 +95,4 @@ for the server's identification before sending KEX-INIT 
message.
 
 * [SSHD-949](https://issues.apache.org/jira/browse/SSHD-949) - Session should 
use cipher block size and not IV size to calculate padding.
 
+* [SSHD-953](https://issues.apache.org/jira/browse/SSHD-953) - Parse and strip 
quoted command arguments when executing a server-side command via local shell.
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/OsUtils.java 
b/sshd-common/src/main/java/org/apache/sshd/common/util/OsUtils.java
index 8663326..d497a48 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/util/OsUtils.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/util/OsUtils.java
@@ -105,12 +105,20 @@ public final class OsUtils {
         }
     }
 
-    public static List<String> resolveDefaultInteractiveCommand() {
-        return resolveInteractiveCommand(isWin32());
+    public static String resolveDefaultInteractiveShellCommand() {
+        return resolveDefaultInteractiveShellCommand(isWin32());
     }
 
-    public static List<String> resolveInteractiveCommand(boolean isWin32) {
-        if (isWin32) {
+    public static String resolveDefaultInteractiveShellCommand(boolean winOS) {
+        return winOS ? WINDOWS_SHELL_COMMAND_NAME : LINUX_SHELL_COMMAND_NAME + 
" -i -l";
+    }
+
+    public static List<String> resolveDefaultInteractiveCommandElements() {
+        return resolveDefaultInteractiveCommandElements(isWin32());
+    }
+
+    public static List<String> 
resolveDefaultInteractiveCommandElements(boolean winOS) {
+        if (winOS) {
             return WINDOWS_COMMAND;
         } else {
             return LINUX_COMMAND;
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/channel/ChannelOutputStream.java
 
b/sshd-core/src/main/java/org/apache/sshd/common/channel/ChannelOutputStream.java
index 7457359..a5168ee 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/common/channel/ChannelOutputStream.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/common/channel/ChannelOutputStream.java
@@ -59,11 +59,15 @@ public class ChannelOutputStream extends OutputStream 
implements java.nio.channe
     private int lastSize;
     private boolean noDelay;
 
-    public ChannelOutputStream(AbstractChannel channel, Window remoteWindow, 
Logger log, byte cmd, boolean eofOnClose) {
-        this(channel, remoteWindow, 
channel.getLongProperty(WAIT_FOR_SPACE_TIMEOUT, 
DEFAULT_WAIT_FOR_SPACE_TIMEOUT), log, cmd, eofOnClose);
+    public ChannelOutputStream(
+            AbstractChannel channel, Window remoteWindow, Logger log, byte 
cmd, boolean eofOnClose) {
+        this(channel, remoteWindow,
+            channel.getLongProperty(WAIT_FOR_SPACE_TIMEOUT, 
DEFAULT_WAIT_FOR_SPACE_TIMEOUT),
+            log, cmd, eofOnClose);
     }
 
-    public ChannelOutputStream(AbstractChannel channel, Window remoteWindow, 
long maxWaitTimeout, Logger log, byte cmd, boolean eofOnClose) {
+    public ChannelOutputStream(
+            AbstractChannel channel, Window remoteWindow, long maxWaitTimeout, 
Logger log, byte cmd, boolean eofOnClose) {
         this.channelInstance = Objects.requireNonNull(channel, "No channel");
         this.packetWriter = 
channelInstance.resolveChannelStreamPacketWriter(channel, cmd);
         this.remoteWindow = Objects.requireNonNull(remoteWindow, "No remote 
window");
@@ -121,7 +125,8 @@ public class ChannelOutputStream extends OutputStream 
implements java.nio.channe
             // packet we sent to allow the producer to race ahead and fill
             // out the next packet before we block and wait for space to
             // become available again.
-            long l2 = Math.min(l, Math.min(remoteWindow.getSize() + lastSize, 
remoteWindow.getPacketSize()) - bufferLength);
+            long minReqLen = Math.min(remoteWindow.getSize() + lastSize, 
remoteWindow.getPacketSize());
+            long l2 = Math.min(l, minReqLen - bufferLength);
             if (l2 <= 0) {
                 if (bufferLength > 0) {
                     flush();
@@ -135,7 +140,7 @@ public class ChannelOutputStream extends OutputStream 
implements java.nio.channe
                     } catch (IOException e) {
                         if (debugEnabled) {
                             log.debug("write({}) failed ({}) to wait for space 
of len={}: {}",
-                                      this, e.getClass().getSimpleName(), l, 
e.getMessage());
+                                  this, e.getClass().getSimpleName(), l, 
e.getMessage());
                         }
 
                         if ((e instanceof WindowClosedException) && 
(!closedState.getAndSet(true))) {
@@ -146,14 +151,17 @@ public class ChannelOutputStream extends OutputStream 
implements java.nio.channe
 
                         throw e;
                     } catch (InterruptedException e) {
-                        throw (IOException) new 
InterruptedIOException("Interrupted while waiting for remote space on write 
len=" + l + " to " + this).initCause(e);
+                        throw (IOException) new InterruptedIOException(
+                            "Interrupted while waiting for remote space on 
write len=" + l + " to " + this)
+                            .initCause(e);
                     }
                 }
                 session.resetIdleTimeout();
                 continue;
             }
 
-            ValidateUtils.checkTrue(l2 <= Integer.MAX_VALUE, "Accumulated 
bytes length exceeds int boundary: %d", l2);
+            ValidateUtils.checkTrue(l2 <= Integer.MAX_VALUE,
+                "Accumulated bytes length exceeds int boundary: %d", l2);
             buffer.putRawBytes(buf, s, (int) l2);
             bufferLength += l2;
             s += l2;
@@ -192,7 +200,7 @@ public class ChannelOutputStream extends OutputStream 
implements java.nio.channe
                 } catch (IOException e) {
                     if (log.isDebugEnabled()) {
                         log.debug("flush({}) failed ({}) to wait for space of 
len={}: {}",
-                                  this, e.getClass().getSimpleName(), total, 
e.getMessage());
+                              this, e.getClass().getSimpleName(), total, 
e.getMessage());
                     }
 
                     throw e;
@@ -222,7 +230,8 @@ public class ChannelOutputStream extends OutputStream 
implements java.nio.channe
                 session.resetIdleTimeout();
                 remoteWindow.waitAndConsume(length, maxWaitTimeout);
                 if (traceEnabled) {
-                    log.trace("flush({}) send {} len={}", channel, 
SshConstants.getCommandMessageName(cmd), length);
+                    log.trace("flush({}) send {} len={}",
+                        channel, SshConstants.getCommandMessageName(cmd), 
length);
                 }
                 packetWriter.writePacket(buf);
             }
@@ -237,7 +246,9 @@ public class ChannelOutputStream extends OutputStream 
implements java.nio.channe
             if (e instanceof IOException) {
                 throw (IOException) e;
             } else if (e instanceof InterruptedException) {
-                throw (IOException) new InterruptedIOException("Interrupted 
while waiting for remote space flush len=" + bufferLength + " to " + 
this).initCause(e);
+                throw (IOException) new InterruptedIOException(
+                    "Interrupted while waiting for remote space flush len=" + 
bufferLength + " to " + this)
+                    .initCause(e);
             } else {
                 throw new SshException(e);
             }
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/server/command/CommandFactory.java 
b/sshd-core/src/main/java/org/apache/sshd/server/command/CommandFactory.java
index dbd4f0c..873c3b4 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/command/CommandFactory.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/command/CommandFactory.java
@@ -19,7 +19,11 @@
 package org.apache.sshd.server.command;
 
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 
+import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.server.channel.ChannelSession;
 
 /**
@@ -30,7 +34,6 @@ import org.apache.sshd.server.channel.ChannelSession;
  */
 @FunctionalInterface
 public interface CommandFactory {
-
     /**
      * Create a command with the given name.
      * If the command is not known, a dummy command should be returned to allow
@@ -43,4 +46,69 @@ public interface CommandFactory {
      * @throws IOException if failed to create the instance
      */
     Command createCommand(ChannelSession channel, String command) throws 
IOException;
+
+    /**
+     * @param command The raw command - ignored if {@code null}/empty
+     * @return The parsed command elements while stripping quoted arguments
+     */
+    static List<String> split(String command) {
+        int len = GenericUtils.length(command);
+        if (len <= 0) {
+            return Collections.emptyList();
+        }
+
+        int curPos = command.indexOf(' ');
+        if (curPos < 0) {
+            return Collections.singletonList(command);
+        }
+
+        int lastPos = 0;
+        List<String> elements = new ArrayList<>();
+        for (curPos = 0; curPos < len; curPos++) {
+            char ch = command.charAt(curPos);
+            // delimited element ?
+            if (((ch == '\'') || (ch == '"'))
+                    // not last character
+                    && (curPos < (len - 1))
+                    // either 1st character or preceded by space
+                    && ((curPos == 0) || (command.charAt(curPos - 1) == ' '))) 
{
+                // find matching delimiter
+                int nextPos = command.indexOf(ch, curPos + 1);
+                if (nextPos <= curPos) {
+                    continue;   // if not found assume unquoted
+                }
+
+                String elem = command.substring(curPos + 1, nextPos);
+                elements.add(elem);
+
+                curPos = nextPos;
+            } else if (ch != ' ') {
+                continue;
+            } else {
+                if (lastPos < curPos) {
+                    String elem = command.substring(lastPos, curPos);
+                    elements.add(elem);
+                }
+            }
+
+            // skip space and any sequence of them
+            for (curPos++; curPos < len; curPos++) {
+                ch = command.charAt(curPos);
+                if (ch != ' ') {
+                    break;
+                }
+            }
+
+            lastPos = curPos;
+            curPos--;   // compensate for loop auto-increment
+        }
+
+        // any trailing element ?
+        if (lastPos < len) {
+            String elem = command.substring(lastPos);
+            elements.add(elem);
+        }
+
+        return elements;
+    }
 }
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/server/shell/InteractiveProcessShellFactory.java
 
b/sshd-core/src/main/java/org/apache/sshd/server/shell/InteractiveProcessShellFactory.java
index 8011bf3..89d74ee 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/server/shell/InteractiveProcessShellFactory.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/server/shell/InteractiveProcessShellFactory.java
@@ -33,11 +33,13 @@ public class InteractiveProcessShellFactory extends 
ProcessShellFactory {
     public static final InteractiveProcessShellFactory INSTANCE = new 
InteractiveProcessShellFactory();
 
     public InteractiveProcessShellFactory() {
-        super(OsUtils.resolveDefaultInteractiveCommand());
+        super(OsUtils.resolveDefaultInteractiveShellCommand(),
+            OsUtils.resolveDefaultInteractiveCommandElements());
     }
 
     @Override
-    protected List<String> resolveEffectiveCommand(ChannelSession channel, 
List<String> original) {
-        return original;
+    protected List<String> resolveEffectiveCommand(
+            ChannelSession channel, String rawCommand, List<String> 
parsedElements) {
+        return parsedElements;
     }
 }
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/server/shell/ProcessShellCommandFactory.java
 
b/sshd-core/src/main/java/org/apache/sshd/server/shell/ProcessShellCommandFactory.java
index 37628dd..fe7c67b 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/server/shell/ProcessShellCommandFactory.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/server/shell/ProcessShellCommandFactory.java
@@ -21,7 +21,6 @@ package org.apache.sshd.server.shell;
 
 import java.io.IOException;
 
-import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.server.channel.ChannelSession;
 import org.apache.sshd.server.command.Command;
 import org.apache.sshd.server.command.CommandFactory;
@@ -45,8 +44,8 @@ public class ProcessShellCommandFactory implements 
CommandFactory {
     }
 
     @Override
-    public Command createCommand(ChannelSession channel,  String command) 
throws IOException {
-        ShellFactory factory = new 
ProcessShellFactory(GenericUtils.split(command, ' '));
+    public Command createCommand(ChannelSession channel, String command) 
throws IOException {
+        ShellFactory factory = new ProcessShellFactory(command, 
CommandFactory.split(command));
         return factory.createShell(channel);
     }
 }
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/server/shell/ProcessShellFactory.java 
b/sshd-core/src/main/java/org/apache/sshd/server/shell/ProcessShellFactory.java
index 35e7f86..196a5d8 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/server/shell/ProcessShellFactory.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/server/shell/ProcessShellFactory.java
@@ -37,30 +37,44 @@ import org.apache.sshd.server.command.Command;
  * @author <a href="mailto:d...@mina.apache.org";>Apache MINA SSHD Project</a>
  */
 public class ProcessShellFactory extends AbstractLoggingBean implements 
ShellFactory {
-    private List<String> command;
+    private String command;
+    private List<String> elements;
 
     public ProcessShellFactory() {
-        this(Collections.emptyList());
+        command = "";
+        elements = Collections.emptyList();
     }
 
-    public ProcessShellFactory(String... command) {
-        this(GenericUtils.isEmpty(command) ? Collections.emptyList() : 
Arrays.asList(command));
+    public ProcessShellFactory(String command, String... elements) {
+        this(command, GenericUtils.isEmpty(elements) ? Collections.emptyList() 
: Arrays.asList(elements));
     }
 
-    public ProcessShellFactory(List<String> command) {
+    public ProcessShellFactory(String command, List<String> elements) {
         this.command = ValidateUtils.checkNotNullAndNotEmpty(command, "No 
command");
+        this.elements = ValidateUtils.checkNotNullAndNotEmpty(elements, "No 
parsed elements");
     }
 
-    public List<String> getCommand() {
+    /**
+     * @return The original unparsed raw command
+     */
+    public String getCommand() {
         return command;
     }
 
-    public void setCommand(String... command) {
-        setCommand(GenericUtils.isEmpty(command) ? Collections.emptyList() : 
Arrays.asList(command));
+    /**
+     * @return The parsed command elements
+     */
+    public List<String> getElements() {
+        return elements;
     }
 
-    public void setCommand(List<String> command) {
+    public void setCommand(String command, String... elements) {
+        setCommand(command, GenericUtils.isEmpty(elements) ? 
Collections.emptyList() : Arrays.asList(elements));
+    }
+
+    public void setCommand(String command, List<String> elements) {
         this.command = ValidateUtils.checkNotNullAndNotEmpty(command, "No 
command");
+        this.elements = ValidateUtils.checkNotNullAndNotEmpty(elements, "No 
parsed elements");
     }
 
     @Override
@@ -70,26 +84,28 @@ public class ProcessShellFactory extends 
AbstractLoggingBean implements ShellFac
     }
 
     protected InvertedShell createInvertedShell(ChannelSession channel) {
-        return new ProcessShell(resolveEffectiveCommand(channel, 
getCommand()));
+        return new ProcessShell(resolveEffectiveCommand(channel, getCommand(), 
getElements()));
     }
 
     protected List<String> resolveEffectiveCommand(
-            ChannelSession channel, List<String> original) {
+            ChannelSession channel, String rawCommand, List<String> 
parsedElements) {
         if (!OsUtils.isWin32()) {
-            return original;
+            return ValidateUtils.checkNotNullAndNotEmpty(parsedElements, "No 
parsed command elements");
         }
 
-        // Turns out that running a command with no arguments works just fine
-        if (GenericUtils.size(original) <= 1) {
-            return original;
+        // Turns out that running a command with no arguments works just fine 
in Windows
+        if (GenericUtils.size(parsedElements) <= 1) {
+            return ValidateUtils.checkNotNullAndNotEmpty(parsedElements, "No 
parsed command elements");
         }
 
         // For windows create a "cmd.exe /C "..."" string
-        String cmdName = original.get(0);
+        String cmdName = parsedElements.get(0);
+        // If already using shell prefix then assume callers knows what 
they're doing
         if (OsUtils.WINDOWS_SHELL_COMMAND_NAME.equalsIgnoreCase(cmdName)) {
-            return original;    // assume callers knows what they're doing
+            return ValidateUtils.checkNotNullAndNotEmpty(parsedElements, "No 
parsed command elements");
         }
 
-        return Arrays.asList(OsUtils.WINDOWS_SHELL_COMMAND_NAME, "/C", 
GenericUtils.join(original, ' '));
+        return Arrays.asList(OsUtils.WINDOWS_SHELL_COMMAND_NAME, "/C",
+            ValidateUtils.checkNotNullAndNotEmpty(rawCommand, "No command"));
     }
 }
diff --git 
a/sshd-core/src/test/java/org/apache/sshd/server/command/CommandFactorySplitterTest.java
 
b/sshd-core/src/test/java/org/apache/sshd/server/command/CommandFactorySplitterTest.java
new file mode 100644
index 0000000..058a49b
--- /dev/null
+++ 
b/sshd-core/src/test/java/org/apache/sshd/server/command/CommandFactorySplitterTest.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.server.command;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory;
+import org.apache.sshd.util.test.JUnitTestSupport;
+import org.apache.sshd.util.test.NoIoTestCase;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.junit.runners.MethodSorters;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runners.Parameterized.UseParametersRunnerFactory;
+
+/**
+ * @author <a href="mailto:d...@mina.apache.org";>Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Category({ NoIoTestCase.class })
+@RunWith(Parameterized.class)   // see 
https://github.com/junit-team/junit/wiki/Parameterized-tests
+@UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class)
+public class CommandFactorySplitterTest extends JUnitTestSupport {
+    private final String command;
+    private final List<String> expected;
+
+    public CommandFactorySplitterTest(String command, List<String> expected) {
+        this.command = command;
+        this.expected = expected;
+    }
+
+    @Parameters(name = "cmd={0}")
+    public static List<Object[]> parameters() {
+        return new ArrayList<Object[]>() {
+            // not serializing it
+            private static final long serialVersionUID = 1L;
+
+            {
+                addTestCase(null, Collections.emptyList());
+                addTestCase("", Collections.emptyList());
+                addTestCase("ls", Collections.singletonList("ls"));
+                addTestCase("ls -l -a --sort /tmp",
+                    Arrays.asList("ls", "-l", "-a", "--sort", "/tmp"));
+                addTestCase("ssh   -o      StrictHostKeyChecking=no  
user@1.2.3.4",
+                    Arrays.asList("ssh", "-o", "StrictHostKeyChecking=no", 
"user@1.2.3.4"));
+                addTestCase("rm -rvf '/tmp/Single Quoted/with spaces'",
+                    Arrays.asList("rm", "-rvf", "/tmp/Single Quoted/with 
spaces"));
+                addTestCase("ls -la \"/tmp/Double Quoted with spaces\"",
+                    Arrays.asList("ls", "-la", "/tmp/Double Quoted with 
spaces"));
+                addTestCase("doWith'Quote\"in'middle something",
+                    Arrays.asList("doWith'Quote\"in'middle", "something"));
+                addTestCase("'Single quoted' first",
+                    Arrays.asList("Single quoted", "first"));
+                addTestCase("\"Double quoted\" first",
+                    Arrays.asList("Double quoted", "first"));
+            }
+
+            private void addTestCase(String cmd, List<String> elems) {
+                add(new Object[]{cmd, elems});
+            }
+        };
+    }
+
+    @Test
+    public void testSplitter() {
+        List<String> actual = CommandFactory.split(command);
+        assertListEquals(command, expected, actual);
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "[" + command + "]";
+    }
+}

Reply via email to