CVE-2021-21242 (Java, published 2021-01-15T21:15Z)
Description: value: 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 v3 base score: 9.8)
Weakness: CWE-502 \u2014 Deserialization of Untrusted Data
The product deserializes untrusted data without sufficiently verifying that the resulting data will be valid.
Repository: https://github.com/theonedev/onedev
Files changed:
server-core/src/main/java/io/onedev/server/CoreModule.java
server-core/src/main/java/io/onedev/server/web/BaseUrlMapper.java
server-core/src/main/java/io/onedev/server/web/component/markdown/MarkdownEditor.java
server-core/src/main/java/io/onedev/server/web/component/markdown/markdown.js
server-product/src/main/java/io/onedev/server/product/ProductServletConfigurator.java
Commit message: Do not use deserialized AttachmentSupport from client side to avoid security vulnerabilities


Vulnerable code:
import io.onedev.server.web.component.markdown.AttachmentUploadServlet;
		bind(AttachmentUploadServlet.class);
		
		add(new DynamicPathPageMapper("projects/${project}/settings/general", GeneralSecuritySettingPage.class));
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);
		}
	}
	
}
import org.apache.commons.lang3.SerializationUtils;
import org.apache.wicket.util.crypt.Base64;
	private AbstractPostAjaxBehavior ajaxBehavior;
		container.add(ajaxBehavior = new AbstractPostAjaxBehavior() {
		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"), 
				callback, 
				encodedAttachmentSupport, 
				getAttachmentSupport()!=null?getAttachmentSupport().getAttachmentMaxSize():0,
	onDomReady: function(containerId, callback, atWhoLimit, attachmentSupport, 
	    if (attachmentSupport) {
							callback("insertUrl", xhr.responseText, xhr.replaceMessage);
									"!!" + xhr.responseText + "!!", xhr.replaceMessage);
					xhr.open("POST", "/attachment_upload", true);
					xhr.setRequestHeader("Attachment-Support", attachmentSupport);
import io.onedev.server.web.component.markdown.AttachmentUploadServlet;
	private final AttachmentUploadServlet attachmentUploadServlet;
	
			WicketServlet wicketServlet, WebSocketManager webSocketManager, 
			AttachmentUploadServlet attachmentUploadServlet, ServletContainer jerseyServlet) {
		this.attachmentUploadServlet = attachmentUploadServlet;
		context.addServlet(new ServletHolder(attachmentUploadServlet), "/attachment_upload");
		

Fixed code:
import io.onedev.server.web.page.project.setting.general.GeneralProjectSettingPage;
		add(new DynamicPathPageMapper("projects/${project}/settings/general", GeneralProjectSettingPage.class));
import java.io.IOException;
import java.net.URLEncoder;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
	private static final Logger logger = LoggerFactory.getLogger(MarkdownEditor.class);
	
	private AbstractPostAjaxBehavior actionBehavior;
	
	private AbstractPostAjaxBehavior attachmentUploadBehavior;
		container.add(actionBehavior = new AbstractPostAjaxBehavior() {
					
		
		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);
				}
			}
			
		});
		String actionCallback = actionBehavior.getCallbackFunction(explicit("action"), explicit("param1"), explicit("param2"), 
		String attachmentUploadUrl = attachmentUploadBehavior.getCallbackUrl().toString();
				actionCallback, 
				getAttachmentSupport()!=null? "'" + attachmentUploadUrl + "'": "undefined", 
				getAttachmentSupport()!=null? getAttachmentSupport().getAttachmentMaxSize(): 0,
	onDomReady: function(containerId, callback, atWhoLimit, attachmentUploadUrl, 
	    if (attachmentUploadUrl) {
						var response = xhr.responseText;
						var index = response.indexOf("<?xml");
						if (index != -1)
							response = response.substring(0, index);
							callback("insertUrl", response, xhr.replaceMessage);
									"!!" + response + "!!", xhr.replaceMessage);
					
					xhr.open("POST", attachmentUploadUrl, true);
					xhr.setRequestHeader("Wicket-Ajax", "true");
					xhr.setRequestHeader("Wicket-Ajax-BaseURL", Wicket.Ajax.baseUrl);
					
			WicketServlet wicketServlet, WebSocketManager webSocketManager, ServletContainer jerseyServlet) {