This is an automated email from the ASF dual-hosted git repository.

xxyu pushed a commit to branch kylin5
in repository https://gitbox.apache.org/repos/asf/kylin.git

commit f9b65d350d55d078e0d2776cb6384e357ef0bddc
Author: Lu Cao <whuca...@gmail.com>
AuthorDate: Mon Jun 5 17:17:33 2023 +0800

    KYLIN-5700 fix shell command injection
    
    ---------
    
    Co-authored-by: Zhiting Guo <35057824+fre...@users.noreply.github.com>
    Co-authored-by: Zhiting Guo <zhiting....@kyligence.io>
---
 .../apache/kylin/rest/service/SystemService.java   |  5 ++
 .../org/apache/kylin/common/util/StringHelper.java | 69 ++++++++++++++++++++++
 .../apache/kylin/common/util/StringHelperTest.java | 65 ++++++++++++++++++++
 .../apache/kylin/source/hive/HiveCmdBuilder.java   |  3 +-
 .../kylin/source/hive/HiveCmdBuilderTest.java      |  2 +-
 .../java/org/apache/kylin/tool/InfluxDBTool.java   |  5 ++
 .../apache/kylin/tool/setup/KapGetClusterInfo.java |  8 ++-
 .../kylin/tool/util/HadoopConfExtractor.java       |  6 +-
 .../kylin/tool/setup/KapGetClusterInfoTest.java    | 38 ++++++++++++
 .../kylin/tool/util/HadoopConfExtractorTest.java   | 42 +++++++++++++
 10 files changed, 235 insertions(+), 8 deletions(-)

diff --git 
a/src/common-service/src/main/java/org/apache/kylin/rest/service/SystemService.java
 
b/src/common-service/src/main/java/org/apache/kylin/rest/service/SystemService.java
index 425928703f..24505237e9 100644
--- 
a/src/common-service/src/main/java/org/apache/kylin/rest/service/SystemService.java
+++ 
b/src/common-service/src/main/java/org/apache/kylin/rest/service/SystemService.java
@@ -29,6 +29,7 @@ import static org.apache.kylin.tool.constant.StageEnum.DONE;
 
 import java.io.File;
 import java.io.IOException;
+import java.util.Arrays;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
@@ -52,6 +53,7 @@ import 
org.apache.kylin.common.persistence.transaction.MessageSynchronization;
 import org.apache.kylin.common.scheduler.EventBusFactory;
 import org.apache.kylin.common.util.BufferedLogger;
 import org.apache.kylin.common.util.CliCommandExecutor;
+import org.apache.kylin.common.util.StringHelper;
 import org.apache.kylin.helper.MetadataToolHelper;
 import org.apache.kylin.job.execution.AbstractExecutable;
 import org.apache.kylin.job.execution.NExecutableManager;
@@ -165,6 +167,9 @@ public class SystemService extends BasicService {
         Future<?> task = executorService.submit(() -> {
             try {
                 exceptionMap.invalidate(uuid);
+                if 
(!Arrays.stream(arguments).allMatch(StringHelper::validateShellArgument)) {
+                    throw new IllegalArgumentException("Shell args have 
illegal char: " + Arrays.toString(arguments));
+                }
                 String finalCommand = String.format(Locale.ROOT, 
"%s/bin/diag.sh %s", KylinConfig.getKylinHome(),
                         StringUtils.join(arguments, " "));
                 commandExecutor.execute(finalCommand, patternedLogger, uuid);
diff --git 
a/src/core-common/src/main/java/org/apache/kylin/common/util/StringHelper.java 
b/src/core-common/src/main/java/org/apache/kylin/common/util/StringHelper.java
index 3754b11a19..7c31565573 100644
--- 
a/src/core-common/src/main/java/org/apache/kylin/common/util/StringHelper.java
+++ 
b/src/core-common/src/main/java/org/apache/kylin/common/util/StringHelper.java
@@ -20,8 +20,11 @@ package org.apache.kylin.common.util;
 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Arrays;
+import java.util.Iterator;
 import java.util.List;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
 import org.apache.commons.lang3.StringUtils;
 
@@ -197,6 +200,72 @@ public class StringHelper {
         return str.split(splitBy);
     }
 
+    public static boolean validateUrl(String s) {
+        return 
Pattern.compile("^(http(s)?://)?[a-zA-Z0-9._-]+(:[0-9]+)?(/[a-zA-Z0-9._-]+)*/?$").matcher(s).matches();
+    }
+
+    public static boolean validateDbName(String s) {
+        return Pattern.compile("^[0-9a-zA-Z_-]+$").matcher(s).matches();
+    }
+
+    public static boolean validateShellArgument(String s) {
+        return Pattern.compile("^[a-zA-Z0-9_./-]+$").matcher(s).matches();
+    }
+
+    public static String escapeShellArguments(String args) {
+        String[] expandArgs = Arrays.stream(args.split(" ")).filter(arg -> 
!arg.isEmpty()).toArray(String[]::new);
+        return String.join(" ", escapeShellArguments(expandArgs));
+    }
+
+    /**
+     * support cases:
+     * -u root
+     * --user root
+     * --user=root
+     */
+    public static String[] escapeShellArguments(String[] args) {
+        Pattern keyPattern = Pattern.compile("^[a-zA-Z0-9-]+$");
+        String key;
+        String value;
+        Iterator<String> argsIterator = Arrays.stream(args).iterator();
+        List<String> newArgs = new ArrayList<>();
+        while (argsIterator.hasNext()) {
+            String cur = argsIterator.next();
+            if (!cur.startsWith("-")) {
+                throw new IllegalArgumentException("Unexpected args found: " + 
Arrays.toString(args));
+            }
+            boolean useEqual = false;
+            if (cur.contains("=")) {
+                useEqual = true;
+                int index = cur.indexOf('=');
+                key = cur.substring(0, index);
+                value = cur.substring(index + 1);
+            } else {
+                key = cur;
+                value = argsIterator.next();
+            }
+            if (!keyPattern.matcher(key).matches()) {
+                throw new IllegalArgumentException("Unexpected args found: " + 
Arrays.toString(args));
+            }
+            // a'b'c -> a b c -> 'a'\''b'\''c'
+            // ''a'b'c'' -> 'a'b'c' -> _ a b c _ -> \\''a'\\''b'\\''c'\\'
+            if (value.startsWith("'") && value.endsWith("'") && value.length() 
>= 2) {
+                value = value.substring(1, value.length() - 1);
+            }
+            String[] splitValues = value.split("'", -1);
+            value = Arrays.stream(splitValues).map(v -> v.isEmpty() ? v : "'" 
+ v + "'")
+                    .collect(Collectors.joining("\\'"));
+
+            if (useEqual) {
+                newArgs.add(key + "=" + value);
+            } else {
+                newArgs.add(key);
+                newArgs.add(value);
+            }
+        }
+        return newArgs.toArray(new String[0]);
+    }
+
     public static String backtickToDoubleQuote(String expression) {
         return convert(expression, StringHelper.BACKTICK, 
StringHelper.DOUBLE_QUOTE);
     }
diff --git 
a/src/core-common/src/test/java/org/apache/kylin/common/util/StringHelperTest.java
 
b/src/core-common/src/test/java/org/apache/kylin/common/util/StringHelperTest.java
index dcadc2a013..517e5e08af 100644
--- 
a/src/core-common/src/test/java/org/apache/kylin/common/util/StringHelperTest.java
+++ 
b/src/core-common/src/test/java/org/apache/kylin/common/util/StringHelperTest.java
@@ -101,4 +101,69 @@ class StringHelperTest {
         Assertions.assertEquals("b", arr[1]);
         Assertions.assertEquals("c", arr[2]);
     }
+
+    @Test
+    void testValidateUrl() {
+        Assertions.assertTrue(StringHelper.validateUrl("127.0.0.1"));
+        Assertions.assertTrue(StringHelper.validateUrl("kylin.apache.org"));
+        Assertions.assertTrue(StringHelper.validateUrl("kylin"));
+        Assertions.assertTrue(StringHelper.validateUrl("http://127.0.0.1";));
+        
Assertions.assertTrue(StringHelper.validateUrl("https://kylin.apache.org";));
+        Assertions.assertTrue(StringHelper.validateUrl("http://kylin";));
+        
Assertions.assertTrue(StringHelper.validateUrl("http://127.0.0.1/a_p.i";));
+        
Assertions.assertTrue(StringHelper.validateUrl("https://kylin.apache.org/api/te-st/";));
+        Assertions.assertTrue(StringHelper.validateUrl("http://kylin/";));
+    }
+
+    @Test
+    void testValidIllegalUrl() {
+        Assertions.assertFalse(StringHelper.validateUrl("http://kylin/$(rm -rf 
/)"));
+        Assertions.assertFalse(StringHelper.validateUrl("http://kylin/`rm 
-rf`"));
+        Assertions.assertFalse(StringHelper.validateUrl("http://kylin/'&ls"));
+        Assertions.assertFalse(StringHelper.validateUrl("http://kylin/;ls";));
+        Assertions.assertFalse(StringHelper.validateUrl("http://kylin/>ls"));
+        Assertions.assertFalse(StringHelper.validateUrl(""));
+    }
+
+    @Test
+    void testValidateDB() {
+        Assertions.assertTrue(StringHelper.validateDbName("db_TEST-01"));
+        Assertions.assertFalse(StringHelper.validateDbName("db&&ls"));
+    }
+
+    @Test
+    void testValidateArgument() {
+        Assertions.assertTrue(StringHelper.validateShellArgument("-job"));
+        
Assertions.assertTrue(StringHelper.validateShellArgument("uuid-uuid-uuid-uuid"));
+        Assertions.assertTrue(StringHelper.validateShellArgument("-JobId"));
+        
Assertions.assertTrue(StringHelper.validateShellArgument("/o-pt.5.0-3/kylin/te_st"));
+
+        Assertions.assertFalse(StringHelper.validateShellArgument("`ls`"));
+        Assertions.assertFalse(StringHelper.validateShellArgument("$(ls)"));
+        Assertions.assertFalse(StringHelper.validateShellArgument("&&"));
+    }
+
+    @Test
+    void testEscapeArguments() {
+        Assertions.assertEquals("", StringHelper.escapeShellArguments(""));
+        Assertions.assertEquals("-u 'root'", 
StringHelper.escapeShellArguments("-u   root  "));
+        Assertions.assertEquals("-u 'root'", 
StringHelper.escapeShellArguments("-u   'root'  "));
+        Assertions.assertEquals("-u \\''root'\\'", 
StringHelper.escapeShellArguments("-u   ''root''  "));
+        Assertions.assertEquals("-u \\'", 
StringHelper.escapeShellArguments("-u '"));
+        Assertions.assertEquals("-u ", StringHelper.escapeShellArguments("-u 
''"));
+        Assertions.assertEquals("-u 'root'", 
StringHelper.escapeShellArguments("-u root"));
+        Assertions.assertEquals("-UserName 'root'", 
StringHelper.escapeShellArguments("-UserName root"));
+        Assertions.assertEquals("-user='root'", 
StringHelper.escapeShellArguments("-user=root"));
+        Assertions.assertEquals("-user='ro=ot'", 
StringHelper.escapeShellArguments("-user=ro=ot"));
+
+        Assertions.assertEquals("-u 'root'\\''$(ls)'", 
StringHelper.escapeShellArguments("-u root'$(ls)"));
+        Assertions.assertEquals("-UserName 'root'\\''$(ls)'",
+                StringHelper.escapeShellArguments("-UserName root'$(ls)"));
+        Assertions.assertEquals("-user='roo'\\''$(ls)t'", 
StringHelper.escapeShellArguments("-user=roo'$(ls)t"));
+        Assertions.assertEquals("-user='roo'\\''$(ls)'\\''t'",
+                StringHelper.escapeShellArguments("-user='roo'$(ls)'t'"));
+
+        Assertions.assertThrows(IllegalArgumentException.class, () -> 
StringHelper.escapeShellArguments("u root"));
+        Assertions.assertThrows(IllegalArgumentException.class, () -> 
StringHelper.escapeShellArguments("-u.ser root"));
+    }
 }
diff --git 
a/src/source-hive/src/main/java/org/apache/kylin/source/hive/HiveCmdBuilder.java
 
b/src/source-hive/src/main/java/org/apache/kylin/source/hive/HiveCmdBuilder.java
index ee7b1c5697..4c68b8b7b1 100644
--- 
a/src/source-hive/src/main/java/org/apache/kylin/source/hive/HiveCmdBuilder.java
+++ 
b/src/source-hive/src/main/java/org/apache/kylin/source/hive/HiveCmdBuilder.java
@@ -23,6 +23,7 @@ import java.util.Locale;
 import java.util.UUID;
 
 import org.apache.kylin.common.KylinConfig;
+import org.apache.kylin.common.util.StringHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -44,7 +45,7 @@ public class HiveCmdBuilder {
     }
 
     public String build() {
-        String beelineParams = kylinConfig.getHiveBeelineParams();
+        String beelineParams = 
StringHelper.escapeShellArguments(kylinConfig.getHiveBeelineParams());
         StringBuilder buf = new StringBuilder();
         String tmpBeelineHqlPath = null;
         StringBuilder beelineHql = new StringBuilder();
diff --git 
a/src/source-hive/src/test/java/org/apache/kylin/source/hive/HiveCmdBuilderTest.java
 
b/src/source-hive/src/test/java/org/apache/kylin/source/hive/HiveCmdBuilderTest.java
index a832be9dc5..9f7e99dee5 100644
--- 
a/src/source-hive/src/test/java/org/apache/kylin/source/hive/HiveCmdBuilderTest.java
+++ 
b/src/source-hive/src/test/java/org/apache/kylin/source/hive/HiveCmdBuilderTest.java
@@ -68,7 +68,7 @@ public class HiveCmdBuilderTest {
 
     private void assertBeelineCmd(String cmd) {
         String beelineCmd = cmd.substring(cmd.indexOf("EOL\n", 
cmd.indexOf("EOL\n") + 1) + 4);
-        assertTrue(beelineCmd.startsWith("beeline -u jdbc_url"));
+        assertTrue(beelineCmd.startsWith("beeline -u 'jdbc_url'"));
     }
 
 }
diff --git a/src/tool/src/main/java/org/apache/kylin/tool/InfluxDBTool.java 
b/src/tool/src/main/java/org/apache/kylin/tool/InfluxDBTool.java
index b728437113..a9564f3aa9 100644
--- a/src/tool/src/main/java/org/apache/kylin/tool/InfluxDBTool.java
+++ b/src/tool/src/main/java/org/apache/kylin/tool/InfluxDBTool.java
@@ -25,6 +25,7 @@ import org.apache.kylin.common.KapConfig;
 import org.apache.kylin.common.KylinConfig;
 import org.apache.kylin.common.util.CliCommandExecutor;
 import org.apache.kylin.common.metrics.service.MonitorDao;
+import org.apache.kylin.common.util.StringHelper;
 import org.apache.kylin.tool.util.ToolUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -72,6 +73,10 @@ public class InfluxDBTool {
                 influxd = new File(influxDBHome + INFLUXD_PATH);
             }
 
+            if (!(StringHelper.validateDbName(database) && 
StringHelper.validateUrl(host))) {
+                throw new IllegalArgumentException(
+                        "Something is wrong with Influx database and host: " + 
database + ", " + host);
+            }
             String cmd = String.format(Locale.ROOT, "%s backup -portable 
-database %s -host %s %s",
                     influxd.getAbsolutePath(), database, host, 
destDir.getAbsolutePath());
             logger.info("InfluxDB backup cmd is {}.", cmd);
diff --git 
a/src/tool/src/main/java/org/apache/kylin/tool/setup/KapGetClusterInfo.java 
b/src/tool/src/main/java/org/apache/kylin/tool/setup/KapGetClusterInfo.java
index d3cf151d59..74d518c843 100644
--- a/src/tool/src/main/java/org/apache/kylin/tool/setup/KapGetClusterInfo.java
+++ b/src/tool/src/main/java/org/apache/kylin/tool/setup/KapGetClusterInfo.java
@@ -24,7 +24,6 @@ import java.nio.charset.Charset;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
 import org.apache.commons.io.FileUtils;
 import org.apache.hadoop.conf.Configuration;
@@ -35,6 +34,7 @@ import org.apache.kylin.common.util.BufferedLogger;
 import org.apache.kylin.common.util.HadoopUtil;
 import org.apache.kylin.common.util.ShellException;
 import org.apache.kylin.cluster.SchedulerInfoCmdHelper;
+import org.apache.kylin.common.util.StringHelper;
 import org.apache.kylin.common.util.Unsafe;
 import org.apache.kylin.tool.util.HadoopConfExtractor;
 import org.slf4j.Logger;
@@ -88,9 +88,8 @@ public class KapGetClusterInfo {
     }
 
     public void extractYarnMasterHost() {
-        Pattern pattern = Pattern.compile("(http://)([^:]*):([^/])*.*");
         if (yarnMasterUrlBase != null) {
-            Matcher m = pattern.matcher(yarnMasterUrlBase);
+            Matcher m = 
HadoopConfExtractor.URL_PATTERN.matcher(yarnMasterUrlBase);
             if (m.matches()) {
                 return;
             }
@@ -106,6 +105,9 @@ public class KapGetClusterInfo {
     public void getYarnMetrics() throws IOException, ShellException, 
YarnException {
         extractYarnMasterHost();
         String url = yarnMasterUrlBase + YARN_METRICS_SUFFIX;
+        if (!StringHelper.validateUrl(url)) {
+            throw new IllegalArgumentException("Url contains disallowed chars, 
url: " + url);
+        }
         String command = "curl -s -k --negotiate -u : " + url;
         KylinConfig config = KylinConfig.getInstanceFromEnv();
         val patternedLogger = new BufferedLogger(logger);
diff --git 
a/src/tool/src/main/java/org/apache/kylin/tool/util/HadoopConfExtractor.java 
b/src/tool/src/main/java/org/apache/kylin/tool/util/HadoopConfExtractor.java
index ab5c2efebe..8683324e64 100644
--- a/src/tool/src/main/java/org/apache/kylin/tool/util/HadoopConfExtractor.java
+++ b/src/tool/src/main/java/org/apache/kylin/tool/util/HadoopConfExtractor.java
@@ -34,6 +34,7 @@ import 
org.apache.kylin.guava30.shaded.common.base.Preconditions;
 
 public class HadoopConfExtractor {
     private static final Logger logger = 
LoggerFactory.getLogger(HadoopConfExtractor.class);
+    public static final Pattern URL_PATTERN = 
Pattern.compile("(http[s]?://)([^:]*):([^/]*).*");
 
     private HadoopConfExtractor() {
     }
@@ -41,9 +42,8 @@ public class HadoopConfExtractor {
     public static String extractYarnMasterUrl(Configuration conf) {
         KylinConfig config = KylinConfig.getInstanceFromEnv();
         final String yarnStatusCheckUrl = config.getYarnStatusCheckUrl();
-        Pattern pattern = Pattern.compile("(http(s)?://)([^:]*):([^/])*.*");
         if (yarnStatusCheckUrl != null) {
-            Matcher m = pattern.matcher(yarnStatusCheckUrl);
+            Matcher m = URL_PATTERN.matcher(yarnStatusCheckUrl);
             if (m.matches()) {
                 return m.group(1) + m.group(2) + ":" + m.group(3);
             }
@@ -77,7 +77,7 @@ public class HadoopConfExtractor {
         if (!rmWebHost.startsWith("http://";) && 
!rmWebHost.startsWith("https://";)) {
             rmWebHost = (YarnConfiguration.useHttps(conf) ? "https://"; : 
"http://";) + rmWebHost;
         }
-        Matcher m = pattern.matcher(rmWebHost);
+        Matcher m = URL_PATTERN.matcher(rmWebHost);
         Preconditions.checkArgument(m.matches(), "Yarn master URL not found.");
         logger.info("yarn master url: {} ", rmWebHost);
         return rmWebHost;
diff --git 
a/src/tool/src/test/java/org/apache/kylin/tool/setup/KapGetClusterInfoTest.java 
b/src/tool/src/test/java/org/apache/kylin/tool/setup/KapGetClusterInfoTest.java
new file mode 100644
index 0000000000..5f4f628574
--- /dev/null
+++ 
b/src/tool/src/test/java/org/apache/kylin/tool/setup/KapGetClusterInfoTest.java
@@ -0,0 +1,38 @@
+/*
+ * 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.kylin.tool.setup;
+
+import org.apache.kylin.common.KylinConfig;
+import org.apache.kylin.junit.annotation.MetadataInfo;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+@MetadataInfo(onlyProps = true)
+class KapGetClusterInfoTest {
+
+    @Test
+    void testGetYarnMetrics() {
+        
KylinConfig.getInstanceFromEnv().setProperty("kylin.job.yarn-app-rest-check-status-url",
+                "http://127.0.0.1:8080`echo`";);
+        Assertions.assertThrows(IllegalArgumentException.class, () -> {
+            KapGetClusterInfo getClusterInfo = new KapGetClusterInfo();
+            getClusterInfo.getYarnMetrics();
+        });
+    }
+}
diff --git 
a/src/tool/src/test/java/org/apache/kylin/tool/util/HadoopConfExtractorTest.java
 
b/src/tool/src/test/java/org/apache/kylin/tool/util/HadoopConfExtractorTest.java
new file mode 100644
index 0000000000..d9f5ea3d26
--- /dev/null
+++ 
b/src/tool/src/test/java/org/apache/kylin/tool/util/HadoopConfExtractorTest.java
@@ -0,0 +1,42 @@
+/*
+ * 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.kylin.tool.util;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.kylin.junit.annotation.MetadataInfo;
+import org.apache.kylin.junit.annotation.OverwriteProp;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+@MetadataInfo(onlyProps = true)
+class HadoopConfExtractorTest {
+
+    @Test
+    @OverwriteProp(key = "kylin.job.yarn-app-rest-check-status-url", value = 
"http://127.0.0.1:8080";)
+    void testExtractYarnMasterHost() {
+        String masterUrl = HadoopConfExtractor.extractYarnMasterUrl(new 
Configuration());
+        Assertions.assertEquals("http://127.0.0.1:8080";, masterUrl);
+    }
+
+    @Test
+    void testDefaultExtractYarnMasterHost() {
+        String masterUrl = HadoopConfExtractor.extractYarnMasterUrl(new 
Configuration());
+        Assertions.assertEquals("http://0.0.0.0:8088";, masterUrl);
+    }
+}

Reply via email to