CVE-2021-21249: Deserialization of Untrusted Data (CWE-502)
value: OneDev is an all-in-one devops platform. In OneDev before version 4.0.3, there is an issue involving YAML parsing which can lead to post-auth remote code execution. In order to parse and process YAML files, OneDev uses SnakeYaml which by default (when not using `SafeConstructor`) allows the instantiation of arbitrary classes. We can leverage that to run arbitrary code by instantiating classes such as `javax.script.ScriptEngineManager` and using `URLClassLoader` to load the script engine provider, resulting in the instantiation of a user controlled class. For a full example refer to the referenced GHSA. This issue was addressed in 4.0.3 by only allowing certain known classes to be deserialized
CWE description: The product deserializes untrusted data without sufficiently verifying that the resulting data will be valid.
CVSS v3 base score: 8.8
Published: 2021-01-15T21:15Z
Repository: https://github.com/theonedev/onedev
Language: Java
Files changed: server-core/src/main/java/io/onedev/server/migration/VersionedYamlDoc.java
Commit date: 2020-11-21 20:16:58 +0800
Commit message: Fix security vulnerability issue caused by SnakeYaml deserialization
Diff: diff --git a/server-core/src/main/java/io/onedev/server/migration/VersionedYamlDoc.java b/server-core/src/main/java/io/onedev/server/migration/VersionedYamlDoc.java
index b08268249b..1bc50481e4 100644
--- a/server-core/src/main/java/io/onedev/server/migration/VersionedYamlDoc.java
+++ b/server-core/src/main/java/io/onedev/server/migration/VersionedYamlDoc.java
@@ -36,12 +36,12 @@ import org.yaml.snakeyaml.resolver.Resolver;
 import org.yaml.snakeyaml.serializer.Serializer;
 
 import edu.emory.mathcs.backport.java.util.Collections;
 import io.onedev.commons.launcher.loader.ImplementationRegistry;
 import io.onedev.commons.utils.ClassUtils;
-import io.onedev.server.OneDev;
 import io.onedev.server.GeneralException;
+import io.onedev.server.OneDev;
 import io.onedev.server.util.BeanUtils;
 import io.onedev.server.web.editable.annotation.Editable;
 
 public class VersionedYamlDoc extends MappingNode {
 
@@ -54,11 +54,10 @@ public class VersionedYamlDoc extends MappingNode {
 	}
 	
 	@SuppressWarnings("unchecked")
 	public <T> T toBean(Class<T> beanClass) {
         setTag(new Tag(beanClass));
-        
 		if (getVersion() != null) {
 			try {
 				MigrationHelper.migrate(getVersion(), beanClass.newInstance(), this);
 				removeVersion();
 			} catch (InstantiationException | IllegalAccessException e) {
@@ -129,21 +128,30 @@ public class VersionedYamlDoc extends MappingNode {
 			return constructDocument(node);
 		}
 
 		@Override
 		protected Class<?> getClassForNode(Node node) {
-			Class<?> type = node.getType();
-			if (type.getAnnotation(Editable.class) != null && !ClassUtils.isConcrete(type)) {
-				ImplementationRegistry registry = OneDev.getInstance(ImplementationRegistry.class);
-				for (Class<?> implementationClass: registry.getImplementations(node.getType())) {
-					String implementationTag = new Tag("!" + implementationClass.getSimpleName()).getValue();
-					if (implementationTag.equals(node.getTag().getValue()))
-						return implementationClass;
+			if (node instanceof VersionedYamlDoc) {
+				return super.getClassForNode(node);
+			} else {
+				Class<?> type = node.getType();
+				if (type.getAnnotation(Editable.class) == null) {
+					// Do not deserialize unknown classes to avoid security vulnerabilities
+					throw new IllegalStateException(String.format("Unexpected yaml node (type: %s, tag: %s)", 
+							type, node.getTag()));
+				} else {
+					if (!ClassUtils.isConcrete(type)) {
+						ImplementationRegistry registry = OneDev.getInstance(ImplementationRegistry.class);
+						for (Class<?> implementationClass: registry.getImplementations(node.getType())) {
+							String implementationTag = new Tag("!" + implementationClass.getSimpleName()).getValue();
+							if (implementationTag.equals(node.getTag().getValue()))
+								return implementationClass;
+						}
+					}
+					return super.getClassForNode(node);
 				}
 			}
-			
-			return super.getClassForNode(node);
 		}
 		
 	}
 	
 	private static class OneYaml extends Yaml {

Vulnerable code: import io.onedev.server.OneDev;
        
			Class<?> type = node.getType();
			if (type.getAnnotation(Editable.class) != null && !ClassUtils.isConcrete(type)) {
				ImplementationRegistry registry = OneDev.getInstance(ImplementationRegistry.class);
				for (Class<?> implementationClass: registry.getImplementations(node.getType())) {
					String implementationTag = new Tag("!" + implementationClass.getSimpleName()).getValue();
					if (implementationTag.equals(node.getTag().getValue()))
						return implementationClass;
			
			return super.getClassForNode(node);
Fixed code: import io.onedev.server.OneDev;
			if (node instanceof VersionedYamlDoc) {
				return super.getClassForNode(node);
			} else {
				Class<?> type = node.getType();
				if (type.getAnnotation(Editable.class) == null) {
					// Do not deserialize unknown classes to avoid security vulnerabilities
					throw new IllegalStateException(String.format("Unexpected yaml node (type: %s, tag: %s)", 
							type, node.getTag()));
				} else {
					if (!ClassUtils.isConcrete(type)) {
						ImplementationRegistry registry = OneDev.getInstance(ImplementationRegistry.class);
						for (Class<?> implementationClass: registry.getImplementations(node.getType())) {
							String implementationTag = new Tag("!" + implementationClass.getSimpleName()).getValue();
							if (implementationTag.equals(node.getTag().getValue()))
								return implementationClass;
						}
					}
					return super.getClassForNode(node);