From e955bacf62fd5417142f7a3128fd272d6cc52d51 Mon Sep 17 00:00:00 2001
From: Jason Song <nobodyiam@gmail.com>
Date: Sat, 11 Jul 2020 14:10:54 +0800
Subject: [PATCH] restrict use of custom yaml types

---
 .../apollo/util/yaml/YamlParser.java          |  7 +-
 .../apollo/util/yaml/YamlParserTest.java      |  6 ++
 .../src/test/resources/yaml/case9.yaml        |  5 ++
 apollo-portal/pom.xml                         | 12 ++-
 .../portal/controller/ItemController.java     | 18 ++++-
 .../portal/controller/ItemControllerTest.java | 76 +++++++++++++++++++
 .../src/test/resources/yaml/case1.yaml        | 25 ++++++
 .../src/test/resources/yaml/case2.yaml        |  4 +
 .../src/test/resources/yaml/case3.yaml        |  5 ++
 9 files changed, 151 insertions(+), 7 deletions(-)
 create mode 100644 apollo-client/src/test/resources/yaml/case9.yaml
 create mode 100644 apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/ItemControllerTest.java
 create mode 100644 apollo-portal/src/test/resources/yaml/case1.yaml
 create mode 100644 apollo-portal/src/test/resources/yaml/case2.yaml
 create mode 100644 apollo-portal/src/test/resources/yaml/case3.yaml

diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/util/yaml/YamlParser.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/util/yaml/YamlParser.java
index d324da91c1c..373017a406b 100644
--- a/apollo-client/src/main/java/com/ctrip/framework/apollo/util/yaml/YamlParser.java
+++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/util/yaml/YamlParser.java
@@ -13,7 +13,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.yaml.snakeyaml.Yaml;
-import org.yaml.snakeyaml.constructor.Constructor;
+import org.yaml.snakeyaml.constructor.SafeConstructor;
 import org.yaml.snakeyaml.nodes.MappingNode;
 import org.yaml.snakeyaml.parser.ParserException;
 
@@ -147,7 +147,10 @@ private interface MatchCallback {
     void process(Properties properties, Map<String, Object> map);
   }
 
-  private static class StrictMapAppenderConstructor extends Constructor {
+  /**
+   * A specialized {@link SafeConstructor} that checks for duplicate keys.
+   */
+  private static class StrictMapAppenderConstructor extends SafeConstructor {
 
     // Declared as public for use in subclasses
     StrictMapAppenderConstructor() {
diff --git a/apollo-client/src/test/java/com/ctrip/framework/apollo/util/yaml/YamlParserTest.java b/apollo-client/src/test/java/com/ctrip/framework/apollo/util/yaml/YamlParserTest.java
index f1adf641856..46262a38ed6 100644
--- a/apollo-client/src/test/java/com/ctrip/framework/apollo/util/yaml/YamlParserTest.java
+++ b/apollo-client/src/test/java/com/ctrip/framework/apollo/util/yaml/YamlParserTest.java
@@ -18,6 +18,7 @@
 import org.mockito.stubbing.Answer;
 import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
 import org.springframework.core.io.ByteArrayResource;
+import org.yaml.snakeyaml.constructor.ConstructorException;
 import org.yaml.snakeyaml.parser.ParserException;
 
 public class YamlParserTest {
@@ -44,6 +45,11 @@ public void testcase8() throws Exception {
     testInvalid("case8.yaml");
   }
 
+  @Test(expected = ConstructorException.class)
+  public void testcase9() throws Exception {
+    testInvalid("case9.yaml");
+  }
+
   @Test
   public void testOrderProperties() throws IOException {
     String yamlContent = loadYaml("orderedcase.yaml");
diff --git a/apollo-client/src/test/resources/yaml/case9.yaml b/apollo-client/src/test/resources/yaml/case9.yaml
new file mode 100644
index 00000000000..9739252c396
--- /dev/null
+++ b/apollo-client/src/test/resources/yaml/case9.yaml
@@ -0,0 +1,5 @@
+!!javax.script.ScriptEngineManager [
+  !!java.net.URLClassLoader [[
+    !!java.net.URL ["http://localhost"]
+  ]]
+]
\ No newline at end of file
diff --git a/apollo-portal/pom.xml b/apollo-portal/pom.xml
index d9588c83c3d..ee5316ab9d0 100644
--- a/apollo-portal/pom.xml
+++ b/apollo-portal/pom.xml
@@ -27,11 +27,12 @@
 			<groupId>com.ctrip.framework.apollo</groupId>
 			<artifactId>apollo-openapi</artifactId>
 		</dependency>
+		<!-- yml processing -->
 		<dependency>
-			<groupId>com.h2database</groupId>
-			<artifactId>h2</artifactId>
-			<scope>test</scope>
+			<groupId>org.yaml</groupId>
+			<artifactId>snakeyaml</artifactId>
 		</dependency>
+		<!-- end of yml processing -->
 		<!-- JDK 1.8+ -->
 		<dependency>
 			<groupId>javax.xml.bind</groupId>
@@ -58,6 +59,11 @@
 		<!-- end of JDK 11+ -->
 
 		<!-- test -->
+		<dependency>
+			<groupId>com.h2database</groupId>
+			<artifactId>h2</artifactId>
+			<scope>test</scope>
+		</dependency>
 		<dependency>
 			<groupId>org.eclipse.jetty</groupId>
 			<artifactId>jetty-server</artifactId>
diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ItemController.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ItemController.java
index b4b0b423c93..0931f1ebc6d 100644
--- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ItemController.java
+++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ItemController.java
@@ -33,6 +33,11 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
+import org.yaml.snakeyaml.DumperOptions;
+import org.yaml.snakeyaml.LoaderOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.constructor.SafeConstructor;
+import org.yaml.snakeyaml.representer.Representer;
 
 import static com.ctrip.framework.apollo.common.utils.RequestPrecondition.checkModel;
 
@@ -208,7 +213,7 @@ public ResponseEntity<Void> syntaxCheckText(@PathVariable String appId, @PathVar
     return ResponseEntity.ok().build();
   }
 
-  private void doSyntaxCheck(NamespaceTextModel model) {
+  void doSyntaxCheck(NamespaceTextModel model) {
     if (StringUtils.isBlank(model.getConfigText())) {
       return;
     }
@@ -219,7 +224,7 @@ private void doSyntaxCheck(NamespaceTextModel model) {
     }
 
     // use YamlPropertiesFactoryBean to check the yaml syntax
-    YamlPropertiesFactoryBean yamlPropertiesFactoryBean = new YamlPropertiesFactoryBean();
+    TypeLimitedYamlPropertiesFactoryBean yamlPropertiesFactoryBean = new TypeLimitedYamlPropertiesFactoryBean();
     yamlPropertiesFactoryBean.setResources(new ByteArrayResource(model.getConfigText().getBytes()));
     // this call converts yaml to properties and will throw exception if the conversion fails
     yamlPropertiesFactoryBean.getObject();
@@ -229,5 +234,14 @@ private boolean isValidItem(ItemDTO item) {
     return Objects.nonNull(item) && !StringUtils.isContainEmpty(item.getKey());
   }
 
+  private static class TypeLimitedYamlPropertiesFactoryBean extends YamlPropertiesFactoryBean {
+    @Override
+    protected Yaml createYaml() {
+      LoaderOptions loaderOptions = new LoaderOptions();
+      loaderOptions.setAllowDuplicateKeys(false);
+      return new Yaml(new SafeConstructor(), new Representer(),
+          new DumperOptions(), loaderOptions);
+    }
+  }
 
 }
diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/ItemControllerTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/ItemControllerTest.java
new file mode 100644
index 00000000000..05f934cb9ca
--- /dev/null
+++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/ItemControllerTest.java
@@ -0,0 +1,76 @@
+package com.ctrip.framework.apollo.portal.controller;
+
+import com.ctrip.framework.apollo.core.enums.ConfigFileFormat;
+import com.ctrip.framework.apollo.portal.component.PermissionValidator;
+import com.ctrip.framework.apollo.portal.entity.model.NamespaceTextModel;
+import com.ctrip.framework.apollo.portal.service.ItemService;
+import com.ctrip.framework.apollo.portal.service.NamespaceService;
+import com.ctrip.framework.apollo.portal.spi.UserInfoHolder;
+import com.google.common.base.Charsets;
+import com.google.common.io.Files;
+import java.io.File;
+import java.io.IOException;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.yaml.snakeyaml.constructor.ConstructorException;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ItemControllerTest {
+
+  @Mock
+  private ItemService configService;
+  @Mock
+  private NamespaceService namespaceService;
+  @Mock
+  private UserInfoHolder userInfoHolder;
+  @Mock
+  private PermissionValidator permissionValidator;
+
+  @InjectMocks
+  private ItemController itemController;
+
+  @Before
+  public void setUp() throws Exception {
+    itemController = new ItemController(configService, userInfoHolder, permissionValidator,
+        namespaceService);
+  }
+
+  @Test
+  public void yamlSyntaxCheckOK() throws Exception {
+    String yaml = loadYaml("case1.yaml");
+
+    itemController.doSyntaxCheck(assemble(ConfigFileFormat.YAML.getValue(), yaml));
+  }
+
+  @Test(expected = IllegalStateException.class)
+  public void yamlSyntaxCheckWithDuplicatedValue() throws Exception {
+    String yaml = loadYaml("case2.yaml");
+
+    itemController.doSyntaxCheck(assemble(ConfigFileFormat.YAML.getValue(), yaml));
+  }
+
+  @Test(expected = ConstructorException.class)
+  public void yamlSyntaxCheckWithUnsupportedType() throws Exception {
+    String yaml = loadYaml("case3.yaml");
+
+    itemController.doSyntaxCheck(assemble(ConfigFileFormat.YAML.getValue(), yaml));
+  }
+
+  private NamespaceTextModel assemble(String format, String content) {
+    NamespaceTextModel model = new NamespaceTextModel();
+    model.setFormat(format);
+    model.setConfigText(content);
+
+    return model;
+  }
+
+  private String loadYaml(String caseName) throws IOException {
+    File file = new File("src/test/resources/yaml/" + caseName);
+
+    return Files.toString(file, Charsets.UTF_8);
+  }
+}
\ No newline at end of file
diff --git a/apollo-portal/src/test/resources/yaml/case1.yaml b/apollo-portal/src/test/resources/yaml/case1.yaml
new file mode 100644
index 00000000000..9153fdc7023
--- /dev/null
+++ b/apollo-portal/src/test/resources/yaml/case1.yaml
@@ -0,0 +1,25 @@
+root:
+  key1: "someValue"
+  key2: 100
+  key3:
+    key4:
+      key5: '(%sender%) %message%'
+      key6: '* %sender% %message%'
+#  commented: "xxx"
+  list:
+  - 'item 1'
+  - 'item 2'
+  intList:
+    - 100
+    - 200
+  listOfMap:
+  - key: '#mychannel'
+    value: ''
+  - key: '#myprivatechannel'
+    value: 'mypassword'
+  listOfList:
+  - - 'a1'
+    - 'a2'
+  - - 'b1'
+    - 'b2'
+  listOfList2: [ ['a1', 'a2'], ['b1', 'b2'] ]
diff --git a/apollo-portal/src/test/resources/yaml/case2.yaml b/apollo-portal/src/test/resources/yaml/case2.yaml
new file mode 100644
index 00000000000..63b49be99ac
--- /dev/null
+++ b/apollo-portal/src/test/resources/yaml/case2.yaml
@@ -0,0 +1,4 @@
+root:
+  key1: "someValue"
+  key2: 100
+  key1: "anotherValue"
diff --git a/apollo-portal/src/test/resources/yaml/case3.yaml b/apollo-portal/src/test/resources/yaml/case3.yaml
new file mode 100644
index 00000000000..9739252c396
--- /dev/null
+++ b/apollo-portal/src/test/resources/yaml/case3.yaml
@@ -0,0 +1,5 @@
+!!javax.script.ScriptEngineManager [
+  !!java.net.URLClassLoader [[
+    !!java.net.URL ["http://localhost"]
+  ]]
+]
\ No newline at end of file