CVE-2021-21242 (published 2021-01-15T21:15Z)

OneDev is an all-in-one devops platform. In OneDev before version 4.0.3, there is a critical vulnerability which can lead to pre-auth remote code execution. AttachmentUploadServlet deserializes untrusted data from the `Attachment-Support` header. This Servlet does not enforce any authentication or authorization checks. This issue may lead to pre-auth remote code execution. This issue was fixed in 4.0.3 by removing AttachmentUploadServlet and not using deserialization

Severity: HIGH
CVSS v2 base score: 7.5 (AV:N/AC:L/Au:N/C:P/I:P/A:P)
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-502
cwe_name: Deserialization of Untrusted Data
description: The product deserializes untrusted data without sufficiently verifying that the resulting data will be valid.
extended_description: 
            It is often convenient to serialize objects for communication or to save them for later use. However, deserialized data or code can often be modified without using the provided accessor functions if it does not use cryptography to protect itself. Furthermore, any cryptography would still be client-side security -- which is a dangerous security assumption.
            Data that is untrusted can not be trusted to be well-formed.
            When developers place no restrictions on "gadget chains," or series of instances and method invocations that can self-execute during the deserialization process (i.e., before the object is returned to the caller), it is sometimes possible for attackers to leverage them to perform unauthorized actions, like generating a shell.
         
         

Fix information:
repo_url: https://github.com/theonedev/onedev
commit_details: repo_url: https://github.com/theonedev/onedev
author: Robin Shen
author_date: 2020-11-18 13:06:37+08:00
msg: Do not use deserialized AttachmentSupport from client side to avoid security vulnerabilities
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: CoreModule.java
change_type: ModificationType.MODIFY
diff: @@ -272,7 +272,6 @@ import io.onedev.server.web.WebApplicationConfigurator;
 import io.onedev.server.web.avatar.AvatarManager;
 import io.onedev.server.web.avatar.DefaultAvatarManager;
 import io.onedev.server.web.component.diff.DiffRenderer;
-import io.onedev.server.web.component.markdown.AttachmentUploadServlet;
 import io.onedev.server.web.component.markdown.SourcePositionTrackExtension;
 import io.onedev.server.web.component.markdown.emoji.EmojiExtension;
 import io.onedev.server.web.component.taskbutton.TaskButton;
@@ -557,8 +556,6 @@ public class CoreModule extends AbstractPluginModule {
 		bind(EditSupportRegistry.class).to(DefaultEditSupportRegistry.class);
 		bind(WebSocketManager.class).to(DefaultWebSocketManager.class);
 
-		bind(AttachmentUploadServlet.class);
-		
 		contributeFromPackage(EditSupport.class, EditSupport.class);
 		
 		bind(org.apache.wicket.protocol.http.WebApplication.class).to(WebApplication.class);

programming_language: Java
filename: BaseUrlMapper.java
change_type: ModificationType.MODIFY
diff: @@ -93,6 +93,7 @@ import io.onedev.server.web.page.project.setting.branchprotection.BranchProtecti
 import io.onedev.server.web.page.project.setting.build.ActionAuthorizationsPage;
 import io.onedev.server.web.page.project.setting.build.BuildPreservationsPage;
 import io.onedev.server.web.page.project.setting.build.JobSecretsPage;
+import io.onedev.server.web.page.project.setting.general.GeneralProjectSettingPage;
 import io.onedev.server.web.page.project.setting.tagprotection.TagProtectionsPage;
 import io.onedev.server.web.page.project.setting.webhook.WebHooksPage;
 import io.onedev.server.web.page.project.stats.ProjectContribsPage;
@@ -260,7 +261,7 @@ public class BaseUrlMapper extends CompoundRequestMapper {
 		add(new DynamicPathPageMapper("projects/${project}/builds/${build}/artifacts", BuildArtifactsPage.class));
 		add(new DynamicPathPageMapper("projects/${project}/builds/${build}/invalid", InvalidBuildPage.class));
 		
-		add(new DynamicPathPageMapper("projects/${project}/settings/general", GeneralSecuritySettingPage.class));
+		add(new DynamicPathPageMapper("projects/${project}/settings/general", GeneralProjectSettingPage.class));
 		add(new DynamicPathPageMapper("projects/${project}/settings/authorizations", ProjectAuthorizationsPage.class));
 		add(new DynamicPathPageMapper("projects/${project}/settings/avatar-edit", AvatarEditPage.class));
 		add(new DynamicPathPageMapper("projects/${project}/settings/branch-protection", BranchProtectionsPage.class));

programming_language: Java
filename: AttachmentUploadServlet.java
change_type: ModificationType.DELETE
diff: @@ -1,48 +0,0 @@
-package io.onedev.server.web.component.markdown;
-
-import java.io.IOException;
-import java.net.URLDecoder;
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
-
-import javax.inject.Singleton;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import org.apache.commons.lang3.SerializationUtils;
-import org.apache.wicket.util.crypt.Base64;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import io.onedev.server.persistence.annotation.Sessional;
-
-@SuppressWarnings("serial")
-@Singleton
-public class AttachmentUploadServlet extends HttpServlet {
-
-	private static final Logger logger = LoggerFactory.getLogger(AttachmentUploadServlet.class);
-	
-	@Sessional
-	@Override
-	protected void doPost(HttpServletRequest request, HttpServletResponse response)
-			throws ServletException, IOException {
-		String fileName = URLDecoder.decode(request.getHeader("File-Name"), StandardCharsets.UTF_8.name());
-		AttachmentSupport attachmentSuppport = (AttachmentSupport) SerializationUtils
-				.deserialize(Base64.decodeBase64(request.getHeader("Attachment-Support")));
-		try {
-			String attachmentName = attachmentSuppport.saveAttachment(fileName, request.getInputStream());
-			response.getWriter().print(URLEncoder.encode(attachmentName, StandardCharsets.UTF_8.name()));
-			response.setStatus(HttpServletResponse.SC_OK);
-		} catch (Exception e) {
-			logger.error("Error uploading attachment.", e);
-			if (e.getMessage() != null)
-				response.getWriter().print(e.getMessage());
-			else
-				response.getWriter().print("Internal server error");
-			response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-		}
-	}
-	
-}

programming_language: unknown
filename: MarkdownEditor.java
change_type: ModificationType.MODIFY
diff: @@ -2,9 +2,11 @@ package io.onedev.server.web.component.markdown;
 
 import static org.apache.wicket.ajax.attributes.CallbackParameter.explicit;
 
+import java.io.IOException;
 import java.io.Serializable;
 import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
+import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -13,8 +15,9 @@ import java.util.Map;
 
 import javax.annotation.Nullable;
 import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
 
-import org.apache.commons.lang3.SerializationUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.text.StringEscapeUtils;
 import org.apache.wicket.AttributeModifier;
@@ -41,7 +44,8 @@ import org.apache.wicket.request.cycle.RequestCycle;
 import org.apache.wicket.request.http.WebRequest;
 import org.apache.wicket.request.mapper.parameter.PageParameters;
 import org.apache.wicket.request.resource.PackageResourceReference;
-import org.apache.wicket.util.crypt.Base64;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.unbescape.javascript.JavaScriptEscape;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
@@ -72,6 +76,8 @@ public class MarkdownEditor extends FormComponentPanel<String> {
 
 	protected static final int ATWHO_LIMIT = 10;
 	
+	private static final Logger logger = LoggerFactory.getLogger(MarkdownEditor.class);
+	
 	private final boolean compactMode;
 	
 	private final boolean initialSplit;
@@ -82,7 +88,9 @@ public class MarkdownEditor extends FormComponentPanel<String> {
 	
 	private TextArea<String> input;
 
-	private AbstractPostAjaxBehavior ajaxBehavior;
+	private AbstractPostAjaxBehavior actionBehavior;
+	
+	private AbstractPostAjaxBehavior attachmentUploadBehavior;
 	
 	/**
 	 * @param id 
@@ -251,7 +259,7 @@ public class MarkdownEditor extends FormComponentPanel<String> {
 		
 		container.add(new WebMarkupContainer("canAttachFile").setVisible(getAttachmentSupport()!=null));
 		
-		container.add(ajaxBehavior = new AbstractPostAjaxBehavior() {
+		container.add(actionBehavior = new AbstractPostAjaxBehavior() {
 
 			@Override
 			protected void updateAjaxAttributes(AjaxRequestAttributes attributes) {
@@ -452,6 +460,7 @@ public class MarkdownEditor extends FormComponentPanel<String> {
 					String replaceMessage = params.getParameterValue("param2").toString();
 					String url = getAttachmentSupport().getAttachmentUrl(name);
 					insertUrl(target, isWebSafeImage(name), url, name, replaceMessage);
+					
 					break;
 				default:
 					throw new IllegalStateException("Unknown action: " + action);
@@ -459,6 +468,34 @@ public class MarkdownEditor extends FormComponentPanel<String> {
 			}
 			
 		});
+		
+		container.add(attachmentUploadBehavior = new AbstractPostAjaxBehavior() {
+			
+			@Override
+			protected void respond(AjaxRequestTarget target) {
+				Preconditions.checkNotNull(getAttachmentSupport(), "Unexpected attachment upload request");
+				HttpServletRequest request = (HttpServletRequest) RequestCycle.get().getRequest().getContainerRequest();
+				HttpServletResponse response = (HttpServletResponse) RequestCycle.get().getResponse().getContainerResponse();
+				try {
+					String fileName = URLDecoder.decode(request.getHeader("File-Name"), StandardCharsets.UTF_8.name());
+					String attachmentName = getAttachmentSupport().saveAttachment(fileName, request.getInputStream());
+					response.getWriter().print(URLEncoder.encode(attachmentName, StandardCharsets.UTF_8.name()));
+					response.setStatus(HttpServletResponse.SC_OK);
+				} catch (Exception e) {
+					logger.error("Error uploading attachment.", e);
+					try {
+						if (e.getMessage() != null)
+							response.getWriter().print(e.getMessage());
+						else
+							response.getWriter().print("Internal server error");
+					} catch (IOException e2) {
+						throw new RuntimeException(e2);
+					}
+					response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+				}
+			}
+			
+		});
 	}
 	
 	@Override
@@ -476,18 +513,9 @@ public class MarkdownEditor extends FormComponentPanel<String> {
 		super.renderHead(response);
 		response.render(JavaScriptHeaderItem.forReference(new MarkdownResourceReference()));
 		
-		String encodedAttachmentSupport;
-		if (getAttachmentSupport() != null) {
-			encodedAttachmentSupport = Base64.encodeBase64String(SerializationUtils.serialize(getAttachmentSupport()));
-			encodedAttachmentSupport = StringUtils.deleteWhitespace(encodedAttachmentSupport);
-			encodedAttachmentSupport = StringEscapeUtils.escapeEcmaScript(encodedAttachmentSupport);
-			encodedAttachmentSupport = "'" + encodedAttachmentSupport + "'";
-		} else {
-			encodedAttachmentSupport = "undefined";
-		}
-		
-		String callback = ajaxBehavior.getCallbackFunction(explicit("action"), explicit("param1"), explicit("param2"), 
+		String actionCallback = actionBehavior.getCallbackFunction(explicit("action"), explicit("param1"), explicit("param2"), 
 				explicit("param3")).toString();
+		String attachmentUploadUrl = attachmentUploadBehavior.getCallbackUrl().toString();
 		
 		String autosaveKey = getAutosaveKey();
 		if (autosaveKey != null)
@@ -497,10 +525,10 @@ public class MarkdownEditor extends FormComponentPanel<String> {
 		
 		String script = String.format("onedev.server.markdown.onDomReady('%s', %s, %d, %s, %d, %b, %b, '%s', %s);", 
 				container.getMarkupId(), 
-				callback, 
+				actionCallback, 
 				ATWHO_LIMIT, 
-				encodedAttachmentSupport, 
-				getAttachmentSupport()!=null?getAttachmentSupport().getAttachmentMaxSize():0,
+				getAttachmentSupport()!=null? "'" + attachmentUploadUrl + "'": "undefined", 
+				getAttachmentSupport()!=null? getAttachmentSupport().getAttachmentMaxSize(): 0,
 				getUserMentionSupport() != null,
 				getReferenceSupport() != null, 
 				JavaScriptEscape.escapeJavaScript(ProjectNameValidator.PATTERN.pattern()),

programming_language: Java
filename: markdown.js
change_type: ModificationType.MODIFY
diff: @@ -14,7 +14,7 @@ onedev.server.markdown = {
 		    $input[0].dispatchEvent(evt);
 		}
 	},
-	onDomReady: function(containerId, callback, atWhoLimit, attachmentSupport, 
+	onDomReady: function(containerId, callback, atWhoLimit, attachmentUploadUrl, 
 			attachmentMaxSize, canMentionUser, canReferenceEntity, 
 			projectNamePattern, autosaveKey) {
 		var $container = $("#" + containerId);
@@ -417,7 +417,7 @@ onedev.server.markdown = {
 		    });		
 	    }
 	    
-	    if (attachmentSupport) {
+	    if (attachmentUploadUrl) {
 	    	var inputEl = $input[0];
 	    	
 			inputEl.addEventListener("paste", function(e) {
@@ -482,20 +482,27 @@ onedev.server.markdown = {
 					}
 					
 					xhr.onload = function() {
+						var response = xhr.responseText;
+						var index = response.indexOf("<?xml");
+						if (index != -1)
+							response = response.substring(0, index);
 						if (xhr.status == 200) { 
-							callback("insertUrl", xhr.responseText, xhr.replaceMessage);
+							callback("insertUrl", response, xhr.replaceMessage);
 						} else { 
 							onedev.server.markdown.updateUploadMessage($input, 
-									"!!" + xhr.responseText + "!!", xhr.replaceMessage);
+									"!!" + response + "!!", xhr.replaceMessage);
 						}
 					};
 					xhr.onerror = function() {
 						onedev.server.markdown.updateUploadMessage($input, 
 								"!!Unable to connect to server!!", xhr.replaceMessage);
 					};
-					xhr.open("POST", "/attachment_upload", true);
+					
+					xhr.open("POST", attachmentUploadUrl, true);
 					xhr.setRequestHeader("File-Name", encodeURIComponent(file.name));
-					xhr.setRequestHeader("Attachment-Support", attachmentSupport);
+					xhr.setRequestHeader("Wicket-Ajax", "true");
+					xhr.setRequestHeader("Wicket-Ajax-BaseURL", Wicket.Ajax.baseUrl);
+					
 					xhr.send(file);
 				}
 			}

programming_language: JavaScript
filename: ProductServletConfigurator.java
change_type: ModificationType.MODIFY
diff: @@ -26,7 +26,6 @@ import io.onedev.server.util.jetty.ClasspathAssetServlet;
 import io.onedev.server.util.jetty.FileAssetServlet;
 import io.onedev.server.util.jetty.ServletConfigurator;
 import io.onedev.server.web.asset.icon.IconScope;
-import io.onedev.server.web.component.markdown.AttachmentUploadServlet;
 import io.onedev.server.web.img.ImageScope;
 import io.onedev.server.web.websocket.WebSocketManager;
 
@@ -44,8 +43,6 @@ public class ProductServletConfigurator implements ServletConfigurator {
 	
 	private final WicketServlet wicketServlet;
 	
-	private final AttachmentUploadServlet attachmentUploadServlet;
-	
 	private final ServletContainer jerseyServlet;
 
 	private final WebSocketManager webSocketManager;
@@ -53,8 +50,7 @@ public class ProductServletConfigurator implements ServletConfigurator {
 	@Inject
 	public ProductServletConfigurator(ServerConfig serverConfig, ShiroFilter shiroFilter, GitFilter gitFilter, 
 			GitPreReceiveCallback preReceiveServlet, GitPostReceiveCallback postReceiveServlet, 
-			WicketServlet wicketServlet, WebSocketManager webSocketManager, 
-			AttachmentUploadServlet attachmentUploadServlet, ServletContainer jerseyServlet) {
+			WicketServlet wicketServlet, WebSocketManager webSocketManager, ServletContainer jerseyServlet) {
 		this.serverConfig = serverConfig;
 		this.shiroFilter = shiroFilter;
         this.gitFilter = gitFilter;
@@ -63,7 +59,6 @@ public class ProductServletConfigurator implements ServletConfigurator {
 		this.wicketServlet = wicketServlet;
 		this.webSocketManager = webSocketManager;
 		this.jerseyServlet = jerseyServlet;
-		this.attachmentUploadServlet = attachmentUploadServlet;
 	}
 	
 	@Override
@@ -88,8 +83,6 @@ public class ProductServletConfigurator implements ServletConfigurator {
 		 */
 		context.addServlet(new ServletHolder(wicketServlet), "/");
 		
-		context.addServlet(new ServletHolder(attachmentUploadServlet), "/attachment_upload");
-		
 		context.addServlet(new ServletHolder(new ClasspathAssetServlet(ImageScope.class)), "/img/*");
 		context.addServlet(new ServletHolder(new ClasspathAssetServlet(IconScope.class)), "/icon/*");
 		

programming_language: Java