CVE-2022-39206 (published 2022-09-13T19:15Z)

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.

CVSS v3 base score: 9.9, CRITICAL (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-610
cwe_name: Externally Controlled Reference to a Resource in Another Sphere
description: The product uses an externally controlled name or reference that resolves to a resource that is outside of the intended control sphere.
extended_description: 
            
         
         

Fix information:
repo_url: https://github.com/theonedev/onedev
commit_details: repo_url: https://github.com/theonedev/onedev
author: Robin Shen
author_date: 2022-05-30 22:21:43+08:00
msg: Fix the docker sock mount security vulnerability
repository: repo_url: https://github.com/theonedev/onedev
repo_name: theonedev/onedev
description: Git Server with CI/CD, Kanban, and Packages. Seamless integration. Unparalleled experience.
repo_language: Java
owner: theonedev
file_changes: filename: pom.xml
change_type: ModificationType.MODIFY
diff: @@ -589,7 +589,7 @@
 	</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>

programming_language: HTML
filename: DataMigrator.java
change_type: ModificationType.MODIFY
diff: @@ -4078,4 +4078,27 @@ 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);
+			}
+		}
+	}
+	
 }

programming_language: Java
filename: KubernetesExecutor.java
change_type: ModificationType.MODIFY
diff: @@ -117,6 +117,8 @@ public class KubernetesExecutor extends JobExecutor implements Testable<TestData
 	
 	private String kubeCtlPath;
 	
+	private boolean mountContainerSock;
+	
 	@Editable(order=20, description="Optionally specify node selector of the job pods")
 	public List<NodeSelectorEntry> getNodeSelector() {
 		return nodeSelector;
@@ -146,6 +148,21 @@ 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, "
@@ -769,8 +786,11 @@ public class KubernetesExecutor extends JobExecutor implements Testable<TestData
 					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) {
@@ -975,14 +995,17 @@ public class KubernetesExecutor extends JobExecutor implements Testable<TestData
 							"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";

programming_language: Java
filename: RemoteDockerExecutor.java
change_type: ModificationType.MODIFY
diff: @@ -36,6 +36,8 @@ public class RemoteDockerExecutor extends ServerDockerExecutor {
 	
 	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)
@@ -47,6 +49,20 @@ public class RemoteDockerExecutor extends ServerDockerExecutor {
 		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);
@@ -73,8 +89,8 @@ public class RemoteDockerExecutor extends ServerDockerExecutor {
 				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);

programming_language: Java
filename: ServerDockerExecutor.java
change_type: ModificationType.MODIFY
diff: @@ -93,6 +93,8 @@ public class ServerDockerExecutor extends JobExecutor implements Testable<TestDa
 	
 	private String dockerExecutable;
 	
+	private boolean mountDockerSock;
+	
 	private static transient volatile String hostInstallPath;
 
 	@Editable(order=400, description="Specify login information for docker registries if necessary")
@@ -104,6 +106,20 @@ public class ServerDockerExecutor extends JobExecutor implements Testable<TestDa
 		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() {
@@ -247,10 +263,12 @@ public class ServerDockerExecutor extends JobExecutor implements Testable<TestDa
 											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());

programming_language: Java
filename: incompatibilities.md
change_type: ModificationType.MODIFY
diff: @@ -1,3 +1,9 @@
+# 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)

programming_language: Markdown