CVE-2022-39206: Externally Controlled Reference to a Resource in Another Sphere (CWE-610)
value: Onedev is an open source, self-hosted Git Server with CI/CD and Kanban. When using Docker-based job executors, the Docker socket (e.g. /var/run/docker.sock on Linux) is mounted into each Docker step. Users that can define and trigger CI/CD jobs on a project could use this to control the Docker daemon on the host machine. This is a known dangerous pattern, as it can be used to break out of Docker containers and, in most cases, gain root privileges on the host system. This issue allows regular (non-admin) users to potentially take over the build infrastructure of a OneDev instance. Attackers need to have an account (or be able to register one) and need permission to create a project. Since code.onedev.io has the right preconditions for this to be exploited by remote attackers, it could have been used to hijack builds of OneDev itself, e.g. by injecting malware into the docker images that are built and pushed to Docker Hub. The impact is increased by this as described before. Users are advised to upgrade to 7.3.0 or higher. There are no known workarounds for this issue.
CWE description: The product uses an externally controlled name or reference that resolves to a resource that is outside of the intended control sphere.
CVSS v3 base score: 9.9
Published: 2022-09-13T19:15Z
Repository: https://github.com/theonedev/onedev
Language: Java
Files changed: pom.xml, server-core/src/main/java/io/onedev/server/migration/DataMigrator.java, server-plugin/server-plugin-executor-kubernetes/src/main/java/io/onedev/server/plugin/executor/kubernetes/KubernetesExecutor.java, server-plugin/server-plugin-executor-remotedocker/src/main/java/io/onedev/server/plugin/executor/remotedocker/RemoteDockerExecutor.java, server-plugin/server-plugin-executor-serverdocker/src/main/java/io/onedev/server/plugin/executor/serverdocker/ServerDockerExecutor.java, server-product/system/incompatibilities/incompatibilities.md
Commit date: 2022-05-30 22:21:43 +0800
Commit message: Fix the docker sock mount security vulnerability
Diff: diff --git a/pom.xml b/pom.xml
index 76468b4dbe5..cd15b3398ac 100644
--- a/pom.xml
+++ b/pom.xml
@@ -587,11 +587,11 @@
 			</snapshots>
 		</repository>
 	</repositories>
 	<properties>
 		<commons.version>2.1.2</commons.version>
-		<agent.version>1.3.7</agent.version>
+		<agent.version>1.3.8</agent.version>
 		<slf4j.version>1.7.30</slf4j.version>
 		<logback.version>1.2.9</logback.version>
 		<antlr.version>4.7.2</antlr.version>
 		<jetty.version>9.4.43.v20210629</jetty.version>
 		<wicket.version>7.17.0</wicket.version>
diff --git a/server-core/src/main/java/io/onedev/server/migration/DataMigrator.java b/server-core/src/main/java/io/onedev/server/migration/DataMigrator.java
index 8b5ab98ad89..c7c870d059c 100644
--- a/server-core/src/main/java/io/onedev/server/migration/DataMigrator.java
+++ b/server-core/src/main/java/io/onedev/server/migration/DataMigrator.java
@@ -4076,6 +4076,29 @@ public class DataMigrator {
 	}
 	
 	private void migrate86(File dataDir, Stack<Integer> versions) {
 	}
 	
+	private void migrate87(File dataDir, Stack<Integer> versions) {
+		for (File file: dataDir.listFiles()) {
+			if (file.getName().startsWith("Settings.xml")) {
+				VersionedXmlDoc dom = VersionedXmlDoc.fromFile(file);
+				for (Element element: dom.getRootElement().elements()) {
+					String key = element.elementTextTrim("key");
+					if (key.equals("JOB_EXECUTORS")) {
+						Element valueElement = element.element("value");
+						if (valueElement != null) {
+							for (Element executorElement: valueElement.elements()) {
+								if (executorElement.getName().contains("DockerExecutor")) 
+									executorElement.addElement("mountDockerSock").setText("false");
+								else if (executorElement.getName().contains("KubernetesExecutor")) 
+									executorElement.addElement("mountContainerSock").setText("false");
+							}
+						}						
+					}
+				}
+				dom.writeToFile(file, false);
+			}
+		}
+	}
+	
 }
diff --git a/server-plugin/server-plugin-executor-kubernetes/src/main/java/io/onedev/server/plugin/executor/kubernetes/KubernetesExecutor.java b/server-plugin/server-plugin-executor-kubernetes/src/main/java/io/onedev/server/plugin/executor/kubernetes/KubernetesExecutor.java
index 07a384f11f2..620cbb423fc 100644
--- a/server-plugin/server-plugin-executor-kubernetes/src/main/java/io/onedev/server/plugin/executor/kubernetes/KubernetesExecutor.java
+++ b/server-plugin/server-plugin-executor-kubernetes/src/main/java/io/onedev/server/plugin/executor/kubernetes/KubernetesExecutor.java
@@ -115,10 +115,12 @@ public class KubernetesExecutor extends JobExecutor implements Testable<TestData
 
 	private String configFile;
 	
 	private String kubeCtlPath;
 	
+	private boolean mountContainerSock;
+	
 	@Editable(order=20, description="Optionally specify node selector of the job pods")
 	public List<NodeSelectorEntry> getNodeSelector() {
 		return nodeSelector;
 	}
 
@@ -144,10 +146,25 @@ public class KubernetesExecutor extends JobExecutor implements Testable<TestData
 	}
 
 	public void setRegistryLogins(List<RegistryLogin> registryLogins) {
 		this.registryLogins = registryLogins;
 	}
+	
+	@Editable(order=300, description="Whether or not to mount docker/containerd sock into job "
+			+ "container to support container operations in job commands, for instance to build "
+			+ "container image.<br>"
+			+ "<b class='text-danger'>WARNING</b>: Malicious jobs can take control of k8s node "
+			+ "running the job by operating the mounted container sock. You should configure job "
+			+ "requirement option below to make sure the executor can only be used by trusted "
+			+ "jobs if this option is enabled")
+	public boolean isMountContainerSock() {
+		return mountContainerSock;
+	}
+
+	public void setMountContainerSock(boolean mountContainerSock) {
+		this.mountContainerSock = mountContainerSock;
+	}
 
 	@Editable(order=25000, group="More Settings", description="Optionally specify where to run service pods "
 			+ "specified in job. The first matching locator will be used. If no any locators are found, "
 			+ "node selector of the executor will be used")
 	public List<ServiceLocator> getServiceLocators() {
@@ -767,12 +784,15 @@ public class KubernetesExecutor extends JobExecutor implements Testable<TestData
 				List<Object> commonVolumeMounts = Lists.<Object>newArrayList(buildHomeMount, authInfoMount, cacheHomeMount);
 				if (osInfo.isWindows())
 					commonVolumeMounts.add(authInfoMount2);
 				if (trustCertsConfigMapName != null)
 					commonVolumeMounts.add(trustCertsMount);
-				commonVolumeMounts.add(dockerSockMount);
-				commonVolumeMounts.add(containerdSockMount);
+				
+				if (isMountContainerSock()) {
+					commonVolumeMounts.add(dockerSockMount);
+					commonVolumeMounts.add(containerdSockMount);
+				}
 
 				CompositeFacade entryFacade;
 				if (jobContext != null) {
 					entryFacade = new CompositeFacade(jobContext.getActions());
 				} else {
@@ -973,18 +993,21 @@ public class KubernetesExecutor extends JobExecutor implements Testable<TestData
 					volumes.add(CollectionUtils.newLinkedHashMap(
 							"name", "trust-certs-home", 
 							"configMap", CollectionUtils.newLinkedHashMap(
 									"name", trustCertsConfigMapName)));
 				}
-				volumes.add(CollectionUtils.newLinkedHashMap(
-						"name", "docker-sock", 
-						"hostPath", CollectionUtils.newLinkedHashMap(
-								"path", dockerSock)));
-				volumes.add(CollectionUtils.newLinkedHashMap(
-						"name", "containerd-sock", 
-						"hostPath", CollectionUtils.newLinkedHashMap(
-								"path", containerdSock)));
+				
+				if (isMountContainerSock()) {
+					volumes.add(CollectionUtils.newLinkedHashMap(
+							"name", "docker-sock", 
+							"hostPath", CollectionUtils.newLinkedHashMap(
+									"path", dockerSock)));
+					volumes.add(CollectionUtils.newLinkedHashMap(
+							"name", "containerd-sock", 
+							"hostPath", CollectionUtils.newLinkedHashMap(
+									"path", containerdSock)));
+				}
 				podSpec.put("volumes", volumes);
 
 				String podName = "job";
 				
 				Map<Object, Object> podDef = CollectionUtils.newLinkedHashMap(
diff --git a/server-plugin/server-plugin-executor-remotedocker/src/main/java/io/onedev/server/plugin/executor/remotedocker/RemoteDockerExecutor.java b/server-plugin/server-plugin-executor-remotedocker/src/main/java/io/onedev/server/plugin/executor/remotedocker/RemoteDockerExecutor.java
index 7b88f39ff89..606b45c5bf1 100644
--- a/server-plugin/server-plugin-executor-remotedocker/src/main/java/io/onedev/server/plugin/executor/remotedocker/RemoteDockerExecutor.java
+++ b/server-plugin/server-plugin-executor-remotedocker/src/main/java/io/onedev/server/plugin/executor/remotedocker/RemoteDockerExecutor.java
@@ -34,10 +34,12 @@ public class RemoteDockerExecutor extends ServerDockerExecutor {
 
 	private static final long serialVersionUID = 1L;
 	
 	private String agentQuery;
 	
+	private boolean mountDockerSock;
+	
 	@Editable(order=390, name="Agent Selector", placeholder="Any agent", 
 			description="Specify agents applicable for this executor")
 	@io.onedev.server.web.editable.annotation.AgentQuery(forExecutor=true)
 	public String getAgentQuery() {
 		return agentQuery;
@@ -45,10 +47,24 @@ public class RemoteDockerExecutor extends ServerDockerExecutor {
 
 	public void setAgentQuery(String agentQuery) {
 		this.agentQuery = agentQuery;
 	}
 
+	@Editable(order=400, description="Whether or not to mount docker sock into job container to "
+			+ "support docker operations in job commands, for instance to build docker image.<br>"
+			+ "<b class='text-danger'>WARNING</b>: Malicious jobs can take control of the agent "
+			+ "running the job by operating the mounted docker sock. You should configure job "
+			+ "requirement option below to make sure the executor can only be used by trusted "
+			+ "jobs if this option is enabled")
+	public boolean isMountDockerSock() {
+		return mountDockerSock;
+	}
+
+	public void setMountDockerSock(boolean mountDockerSock) {
+		this.mountDockerSock = mountDockerSock;
+	}
+	
 	@Override
 	public void execute(String jobToken, JobContext jobContext) {
 		AgentQuery parsedQeury = AgentQuery.parse(agentQuery, true);
 		TaskLogger jobLogger = jobContext.getLogger();
 		OneDev.getInstance(ResourceManager.class).run(new AgentAwareRunnable() {
@@ -71,12 +87,12 @@ public class RemoteDockerExecutor extends ServerDockerExecutor {
 					services.add(service.toMap());
 				
 				List<String> trustCertContent = getTrustCertContent();
 				DockerJobData jobData = new DockerJobData(jobToken, getName(), jobContext.getProjectPath(), 
 						jobContext.getProjectId(), jobContext.getCommitId().name(), jobContext.getBuildNumber(), 
-						jobContext.getActions(), jobContext.getRetried(), services, registryLogins, 
-						trustCertContent, getRunOptions());
+						jobContext.getActions(), jobContext.getRetried(), services, registryLogins,
+						mountDockerSock, trustCertContent, getRunOptions());
 				
 				try {
 					WebsocketUtils.call(agentSession, jobData, 0);
 				} catch (InterruptedException | TimeoutException e) {
 					new Message(MessageType.CANCEL_JOB, jobToken).sendBy(agentSession);
diff --git a/server-plugin/server-plugin-executor-serverdocker/src/main/java/io/onedev/server/plugin/executor/serverdocker/ServerDockerExecutor.java b/server-plugin/server-plugin-executor-serverdocker/src/main/java/io/onedev/server/plugin/executor/serverdocker/ServerDockerExecutor.java
index 9633b060dab..f71b563d1f5 100644
--- a/server-plugin/server-plugin-executor-serverdocker/src/main/java/io/onedev/server/plugin/executor/serverdocker/ServerDockerExecutor.java
+++ b/server-plugin/server-plugin-executor-serverdocker/src/main/java/io/onedev/server/plugin/executor/serverdocker/ServerDockerExecutor.java
@@ -91,10 +91,12 @@ public class ServerDockerExecutor extends JobExecutor implements Testable<TestDa
 	
 	private String runOptions;
 	
 	private String dockerExecutable;
 	
+	private boolean mountDockerSock;
+	
 	private static transient volatile String hostInstallPath;
 
 	@Editable(order=400, description="Specify login information for docker registries if necessary")
 	public List<RegistryLogin> getRegistryLogins() {
 		return registryLogins;
@@ -102,10 +104,24 @@ public class ServerDockerExecutor extends JobExecutor implements Testable<TestDa
 
 	public void setRegistryLogins(List<RegistryLogin> registryLogins) {
 		this.registryLogins = registryLogins;
 	}
 
+	@Editable(order=500, description="Whether or not to mount docker sock into job container to "
+			+ "support docker operations in job commands, for instance to build docker image.<br>"
+			+ "<b class='text-danger'>WARNING</b>: Malicious jobs can take control of whole OneDev "
+			+ "by operating the mounted docker sock. You should configure job requirement "
+			+ "option below to make sure the executor can only be used by trusted jobs if this "
+			+ "option is enabled")
+	public boolean isMountDockerSock() {
+		return mountDockerSock;
+	}
+
+	public void setMountDockerSock(boolean mountDockerSock) {
+		this.mountDockerSock = mountDockerSock;
+	}
+
 	@Editable(order=50050, group="More Settings", description="Optionally specify options to run container. For instance, you may use <tt>-m 2g</tt> "
 			+ "to limit memory of created container to be 2 giga bytes")
 	public String getRunOptions() {
 		return runOptions;
 	}
@@ -245,14 +261,16 @@ public class ServerDockerExecutor extends JobExecutor implements Testable<TestDa
 											String hostCachePath = entry.getKey().getDirectory(hostCacheHome).getAbsolutePath();
 											String containerCachePath = PathUtils.resolve(containerWorkspace, entry.getValue());
 											docker.addArgs("-v", getHostPath(hostCachePath) + ":" + containerCachePath);
 										}
 										
-										if (SystemUtils.IS_OS_WINDOWS) 
-											docker.addArgs("-v", "//./pipe/docker_engine://./pipe/docker_engine");
-										else
-											docker.addArgs("-v", "/var/run/docker.sock:/var/run/docker.sock");
+										if (isMountDockerSock()) {
+											if (SystemUtils.IS_OS_WINDOWS) 
+												docker.addArgs("-v", "//./pipe/docker_engine://./pipe/docker_engine");
+											else
+												docker.addArgs("-v", "/var/run/docker.sock:/var/run/docker.sock");
+										}
 										
 										if (hostAuthInfoHome.get() != null) {
 											String hostPath = getHostPath(hostAuthInfoHome.get().getAbsolutePath());
 											if (SystemUtils.IS_OS_WINDOWS) {
 												docker.addArgs("-v",  hostPath + ":C:\\Users\\ContainerAdministrator\\auth-info");
diff --git a/server-product/system/incompatibilities/incompatibilities.md b/server-product/system/incompatibilities/incompatibilities.md
index facd69bb00c..e2878ad3347 100644
--- a/server-product/system/incompatibilities/incompatibilities.md
+++ b/server-product/system/incompatibilities/incompatibilities.md
@@ -1,5 +1,11 @@
+# 7.3.0
+1. [CI/CD] Docker sock is mounted by default for server docker executor, remote docker executor and Kubernetes 
+executor for security reasons. If your CI job performs docker operation, the build may fail. You may enable 
+the mount docker sock option in related executors, but make sure to configure job requirement of the executor 
+to only allow trusted jobs to use the executors
+
 # 7.0.0
 
 1. [RESTful api] Email addresses of a user should now be retrieved via [UserResource.getEmailAddresses](/help/api/io.onedev.server.rest.UserResource/getEmailAddresses), and should be operated via [EmailAddressResource](/help/api/io.onedev.server.rest.EmailAddressResource)
 2. [RESTful api] Access token of a user should now be retrieved via [UserResource.getAccessToken](/help/api/io.onedev.server.rest.UserResource/getAccessToken)
 3. [RESTful api] User query by login name, full name and email should now be done via [UserResource.queryProfile](/help/api/io.onedev.server.rest.UserResource/queryProfile)

Vulnerable code: 		<agent.version>1.3.7</agent.version>
				commonVolumeMounts.add(dockerSockMount);
				commonVolumeMounts.add(containerdSockMount);
				volumes.add(CollectionUtils.newLinkedHashMap(
						"name", "docker-sock", 
						"hostPath", CollectionUtils.newLinkedHashMap(
								"path", dockerSock)));
				volumes.add(CollectionUtils.newLinkedHashMap(
						"name", "containerd-sock", 
						"hostPath", CollectionUtils.newLinkedHashMap(
								"path", containerdSock)));
						jobContext.getActions(), jobContext.getRetried(), services, registryLogins, 
						trustCertContent, getRunOptions());
										if (SystemUtils.IS_OS_WINDOWS) 
											docker.addArgs("-v", "//./pipe/docker_engine://./pipe/docker_engine");
										else
											docker.addArgs("-v", "/var/run/docker.sock:/var/run/docker.sock");
Fixed code: 		<agent.version>1.3.8</agent.version>
	private void migrate87(File dataDir, Stack<Integer> versions) {
		for (File file: dataDir.listFiles()) {
			if (file.getName().startsWith("Settings.xml")) {
				VersionedXmlDoc dom = VersionedXmlDoc.fromFile(file);
				for (Element element: dom.getRootElement().elements()) {
					String key = element.elementTextTrim("key");
					if (key.equals("JOB_EXECUTORS")) {
						Element valueElement = element.element("value");
						if (valueElement != null) {
							for (Element executorElement: valueElement.elements()) {
								if (executorElement.getName().contains("DockerExecutor")) 
									executorElement.addElement("mountDockerSock").setText("false");
								else if (executorElement.getName().contains("KubernetesExecutor")) 
									executorElement.addElement("mountContainerSock").setText("false");
							}
						}						
					}
				}
				dom.writeToFile(file, false);
			}
		}
	}
	
	private boolean mountContainerSock;
	
	
	@Editable(order=300, description="Whether or not to mount docker/containerd sock into job "
			+ "container to support container operations in job commands, for instance to build "
			+ "container image.<br>"
			+ "<b class='text-danger'>WARNING</b>: Malicious jobs can take control of k8s node "
			+ "running the job by operating the mounted container sock. You should configure job "
			+ "requirement option below to make sure the executor can only be used by trusted "
			+ "jobs if this option is enabled")
	public boolean isMountContainerSock() {
		return mountContainerSock;
	}

	public void setMountContainerSock(boolean mountContainerSock) {
		this.mountContainerSock = mountContainerSock;
	}
				
				if (isMountContainerSock()) {
					commonVolumeMounts.add(dockerSockMount);
					commonVolumeMounts.add(containerdSockMount);
				}
				
				if (isMountContainerSock()) {
					volumes.add(CollectionUtils.newLinkedHashMap(
							"name", "docker-sock", 
							"hostPath", CollectionUtils.newLinkedHashMap(
									"path", dockerSock)));
					volumes.add(CollectionUtils.newLinkedHashMap(
							"name", "containerd-sock", 
							"hostPath", CollectionUtils.newLinkedHashMap(
									"path", containerdSock)));
				}
	private boolean mountDockerSock;
	
	@Editable(order=400, description="Whether or not to mount docker sock into job container to "
			+ "support docker operations in job commands, for instance to build docker image.<br>"
			+ "<b class='text-danger'>WARNING</b>: Malicious jobs can take control of the agent "
			+ "running the job by operating the mounted docker sock. You should configure job "
			+ "requirement option below to make sure the executor can only be used by trusted "
			+ "jobs if this option is enabled")
	public boolean isMountDockerSock() {
		return mountDockerSock;
	}

	public void setMountDockerSock(boolean mountDockerSock) {
		this.mountDockerSock = mountDockerSock;
	}
	
						jobContext.getActions(), jobContext.getRetried(), services, registryLogins,
						mountDockerSock, trustCertContent, getRunOptions());
	private boolean mountDockerSock;
	
	@Editable(order=500, description="Whether or not to mount docker sock into job container to "
			+ "support docker operations in job commands, for instance to build docker image.<br>"
			+ "<b class='text-danger'>WARNING</b>: Malicious jobs can take control of whole OneDev "
			+ "by operating the mounted docker sock. You should configure job requirement "
			+ "option below to make sure the executor can only be used by trusted jobs if this "
			+ "option is enabled")
	public boolean isMountDockerSock() {
		return mountDockerSock;
	}

	public void setMountDockerSock(boolean mountDockerSock) {
		this.mountDockerSock = mountDockerSock;
	}

										if (isMountDockerSock()) {
											if (SystemUtils.IS_OS_WINDOWS) 
												docker.addArgs("-v", "//./pipe/docker_engine://./pipe/docker_engine");
											else
												docker.addArgs("-v", "/var/run/docker.sock:/var/run/docker.sock");
										}
# 7.3.0
1. [CI/CD] Docker sock is mounted by default for server docker executor, remote docker executor and Kubernetes 
executor for security reasons. If your CI job performs docker operation, the build may fail. You may enable 
the mount docker sock option in related executors, but make sure to configure job requirement of the executor 
to only allow trusted jobs to use the executors