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

Onedev is an open source, self-hosted Git Server with CI/CD and Kanban. In versions of Onedev prior to 7.3.0 unauthenticated users can take over a OneDev instance if there is no properly configured reverse proxy. The /git-prereceive-callback endpoint is used by the pre-receive git hook on the server to check for branch protections during a push event. It is only intended to be accessed from localhost, but the check relies on the X-Forwarded-For header. Invoking this endpoint leads to the execution of one of various git commands. The environment variables of this command execution can be controlled via query parameters. This allows attackers to write to arbitrary files, which can in turn lead to the execution of arbitrary code. Such an attack would be very hard to detect, which increases the potential impact even more. Users are advised to upgrade. There are no known workarounds for this issue.

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

Weakness information:
cwe_id: CWE-287
cwe_name: Improper Authentication
description: When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.
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 19:36:08+08:00
msg: Fix security vulnerability related to Git pre/post receive callback
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: DefaultProjectManager.java
change_type: ModificationType.MODIFY
diff: @@ -537,7 +537,7 @@ public class DefaultProjectManager extends BaseEntityManager<Project>
         
         try {
 			String content = FileUtils.readFileToString(hookFile, Charset.defaultCharset());
-			if (!content.contains("ENV_GIT_ALTERNATE_OBJECT_DIRECTORIES"))
+			if (!content.contains("ONEDEV_HOOK_TOKEN"))
 				return false;
 		} catch (IOException e) {
 			throw new RuntimeException(e);

programming_language: Java
filename: GitFilter.java
change_type: ModificationType.MODIFY
diff: @@ -143,6 +143,7 @@ public class GitFilter implements Filter {
 	        environments.put("ONEDEV_CURL", settingManager.getSystemSetting().getCurlConfig().getExecutable());
 			environments.put("ONEDEV_URL", serverUrl);
 			environments.put("ONEDEV_USER_ID", SecurityUtils.getUserId().toString());
+			environments.put("ONEDEV_HOOK_TOKEN", GitUtils.HOOK_TOKEN);
 			environments.put("ONEDEV_REPOSITORY_ID", project.getId().toString());
 			
 			// to be compatible with old repository

programming_language: Java
filename: GitSshCommand.java
change_type: ModificationType.MODIFY
diff: @@ -75,6 +75,7 @@ abstract class GitSshCommand implements Command, ServerSessionAware {
         SettingManager settingManager = OneDev.getInstance(SettingManager.class);
         environments.put("ONEDEV_CURL", settingManager.getSystemSetting().getCurlConfig().getExecutable());
 		environments.put("ONEDEV_URL", serverUrl);
+		environments.put("ONEDEV_HOOK_TOKEN", GitUtils.HOOK_TOKEN);
 		environments.put("ONEDEV_USER_ID", SecurityUtils.getUserId().toString());
 		environments.put("ONEDEV_REPOSITORY_ID", project.getId().toString());
 		return environments;

programming_language: Java
filename: GitUtils.java
change_type: ModificationType.MODIFY
diff: @@ -16,6 +16,7 @@ import java.util.Map;
 
 import javax.annotation.Nullable;
 
+import org.apache.commons.lang3.RandomStringUtils;
 import org.bouncycastle.bcpg.ArmoredOutputStream;
 import org.bouncycastle.bcpg.BCPGOutputStream;
 import org.bouncycastle.bcpg.HashAlgorithmTags;
@@ -97,6 +98,8 @@ public class GitUtils {
 	
 	private static final Logger logger = LoggerFactory.getLogger(GitUtils.class);
 	
+	public static final String HOOK_TOKEN = RandomStringUtils.randomAlphanumeric(20); 
+	
 	public static boolean isEmptyPath(String path) {
 		return Strings.isNullOrEmpty(path) || Objects.equal(path, DiffEntry.DEV_NULL);
 	}

programming_language: Java
filename: GitPostReceiveCallback.java
change_type: ModificationType.MODIFY
diff: @@ -1,7 +1,6 @@
 package io.onedev.server.git.hookcallback;
 
 import java.io.IOException;
-import java.net.InetAddress;
 import java.util.ArrayList;
 import java.util.Enumeration;
 import java.util.List;
@@ -56,17 +55,14 @@ public class GitPostReceiveCallback extends HttpServlet {
     @Sessional
     @Override
 	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
-        String clientIp = request.getHeader("X-Forwarded-For");
-        if (clientIp == null) clientIp = request.getRemoteAddr();
-
-        if (!InetAddress.getByName(clientIp).isLoopbackAddress()) {
+        List<String> fields = StringUtils.splitAndTrim(request.getPathInfo(), "/");
+        Preconditions.checkState(fields.size() == 3);
+        
+        if (!fields.get(2).equals(GitUtils.HOOK_TOKEN)) {
             response.sendError(HttpServletResponse.SC_FORBIDDEN,
-                    "Git hook callbacks can only be accessed from localhost.");
+                    "Git hook callbacks can only be accessed by OneDev itself");
             return;
         }
-
-        List<String> fields = StringUtils.splitAndTrim(request.getPathInfo(), "/");
-        Preconditions.checkState(fields.size() == 2);
         
         Project project = projectManager.load(Long.valueOf(fields.get(0)));
         Long userId = Long.valueOf(fields.get(1));

programming_language: Java
filename: GitPreReceiveCallback.java
change_type: ModificationType.MODIFY
diff: @@ -1,7 +1,6 @@
 package io.onedev.server.git.hookcallback;
 
 import java.io.IOException;
-import java.net.InetAddress;
 import java.util.ArrayList;
 import java.util.Enumeration;
 import java.util.HashMap;
@@ -71,18 +70,15 @@ public class GitPreReceiveCallback extends HttpServlet {
 	@Sessional
 	@Override
 	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
-        String clientIp = request.getHeader("X-Forwarded-For");
-        if (clientIp == null) clientIp = request.getRemoteAddr();
-
-        if (!InetAddress.getByName(clientIp).isLoopbackAddress()) {
+        List<String> fields = StringUtils.splitAndTrim(request.getPathInfo(), "/");
+        Preconditions.checkState(fields.size() == 3);
+        
+        if (!fields.get(2).equals(GitUtils.HOOK_TOKEN)) {
             response.sendError(HttpServletResponse.SC_FORBIDDEN,
-                    "Git hook callbacks can only be accessed from localhost.");
+                    "Git hook callbacks can only be accessed by OneDev itself");
             return;
         }
         
-        List<String> fields = StringUtils.splitAndTrim(request.getPathInfo(), "/");
-        Preconditions.checkState(fields.size() == 2);
-        
         SecurityUtils.getSubject().runAs(SecurityUtils.asPrincipal(Long.valueOf(fields.get(1))));
         try {
             Project project = projectManager.load(Long.valueOf(fields.get(0)));

programming_language: Java
filename: git-receive-hook
change_type: ModificationType.MODIFY
diff: @@ -2,7 +2,7 @@
 unset http_proxy
 unset https_proxy
 IFS=$'\r\n'; 
-lines=($(${ONEDEV_CURL} -k -s -S -f -X POST --data-urlencode "ENV_GIT_ALTERNATE_OBJECT_DIRECTORIES=${GIT_ALTERNATE_OBJECT_DIRECTORIES}" --data-urlencode "ENV_GIT_OBJECT_DIRECTORY=${GIT_OBJECT_DIRECTORY}" --data-urlencode "ENV_GIT_QUARANTINE_PATH=${GIT_QUARANTINE_PATH}" -d @- ${ONEDEV_URL}/%s/${ONEDEV_REPOSITORY_ID}/${ONEDEV_USER_ID} 2>&1))
+lines=($(${ONEDEV_CURL} -k -s -S -f -X POST --data-urlencode "ENV_GIT_ALTERNATE_OBJECT_DIRECTORIES=${GIT_ALTERNATE_OBJECT_DIRECTORIES}" --data-urlencode "ENV_GIT_OBJECT_DIRECTORY=${GIT_OBJECT_DIRECTORY}" --data-urlencode "ENV_GIT_QUARANTINE_PATH=${GIT_QUARANTINE_PATH}" -d @- ${ONEDEV_URL}/%s/${ONEDEV_REPOSITORY_ID}/${ONEDEV_USER_ID}/${ONEDEV_HOOK_TOKEN} 2>&1))
 
 returnCode=0;
 

programming_language: Shell