CVE-2021-21242: Deserialization of Untrusted Data (CWE-502)
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
CWE description: The product deserializes untrusted data without sufficiently verifying that the resulting data will be valid.
CVSS v3 base score: 9.8
Published: 2021-01-15T21:15Z
Repository: https://github.com/theonedev/onedev
Language: Java
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 date: 2020-11-18 14:55:50 +0800
Commit message: Do not use deserialized AttachmentSupport from client side to avoid security vulnerabilities

Diff: diff --git a/server-core/src/main/java/io/onedev/server/CoreModule.java b/server-core/src/main/java/io/onedev/server/CoreModule.java
index f707e52e6..fdcd53882 100644
--- a/server-core/src/main/java/io/onedev/server/CoreModule.java
+++ b/server-core/src/main/java/io/onedev/server/CoreModule.java
@@ -270,11 +270,10 @@ import io.onedev.server.web.ResourcePackScopeContribution;
 import io.onedev.server.web.WebApplication;
 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;
 import io.onedev.server.web.editable.DefaultEditSupportRegistry;
 import io.onedev.server.web.editable.EditSupport;
@@ -555,12 +554,10 @@ public class CoreModule extends AbstractPluginModule {
 		bind(WicketFilter.class).to(DefaultWicketFilter.class);
 		bind(WebSocketPolicy.class).toProvider(WebSocketPolicyProvider.class);
 		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);
 		bind(Application.class).to(WebApplication.class);
 		bind(AvatarManager.class).to(DefaultAvatarManager.class);
diff --git a/server-core/src/main/java/io/onedev/server/web/BaseUrlMapper.java b/server-core/src/main/java/io/onedev/server/web/BaseUrlMapper.java
index aee9ed158..30ed811ff 100644
--- a/server-core/src/main/java/io/onedev/server/web/BaseUrlMapper.java
+++ b/server-core/src/main/java/io/onedev/server/web/BaseUrlMapper.java
@@ -91,10 +91,11 @@ import io.onedev.server.web.page.project.setting.authorization.ProjectAuthorizat
 import io.onedev.server.web.page.project.setting.avatar.AvatarEditPage;
 import io.onedev.server.web.page.project.setting.branchprotection.BranchProtectionsPage;
 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;
 import io.onedev.server.web.page.project.stats.SourceLinesPage;
 import io.onedev.server.web.page.project.tags.ProjectTagsPage;
@@ -258,11 +259,11 @@ public class BaseUrlMapper extends CompoundRequestMapper {
 		add(new DynamicPathPageMapper("projects/${project}/builds/${build}/changes", BuildChangesPage.class));
 		add(new DynamicPathPageMapper("projects/${project}/builds/${build}/fixed-issues", FixedIssuesPage.class));
 		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));
 		add(new DynamicPathPageMapper("projects/${project}/settings/tag-protection", TagProtectionsPage.class));
 		add(new DynamicPathPageMapper("projects/${project}/settings/build/job-secrets", JobSecretsPage.class));
diff --git a/server-core/src/main/java/io/onedev/server/web/component/markdown/AttachmentUploadServlet.java b/server-core/src/main/java/io/onedev/server/web/component/markdown/AttachmentUploadServlet.java
deleted file mode 100644
index 96f90d588..000000000
--- a/server-core/src/main/java/io/onedev/server/web/component/markdown/AttachmentUploadServlet.java
+++ /dev/null
@@ -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);
-		}
-	}
-	
-}
diff --git a/server-core/src/main/java/io/onedev/server/web/component/markdown/MarkdownEditor.java b/server-core/src/main/java/io/onedev/server/web/component/markdown/MarkdownEditor.java
index 3373c882e..371e157b2 100644
--- a/server-core/src/main/java/io/onedev/server/web/component/markdown/MarkdownEditor.java
+++ b/server-core/src/main/java/io/onedev/server/web/component/markdown/MarkdownEditor.java
@@ -1,22 +1,25 @@
 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;
 import java.util.List;
 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;
 import org.apache.wicket.Component;
 import org.apache.wicket.ajax.AjaxChannel;
@@ -39,11 +42,12 @@ import org.apache.wicket.model.Model;
 import org.apache.wicket.request.IRequestParameters;
 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;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.base.Preconditions;
@@ -70,21 +74,25 @@ import io.onedev.server.web.page.project.blob.render.BlobRenderContext;
 @SuppressWarnings("serial")
 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;
 	
 	private final BlobRenderContext blobRenderContext;
 	
 	private WebMarkupContainer container;
 	
 	private TextArea<String> input;
 
-	private AbstractPostAjaxBehavior ajaxBehavior;
+	private AbstractPostAjaxBehavior actionBehavior;
+	
+	private AbstractPostAjaxBehavior attachmentUploadBehavior;
 	
 	/**
 	 * @param id 
 	 * 			component id of the editor
 	 * @param model
@@ -249,11 +257,11 @@ public class MarkdownEditor extends FormComponentPanel<String> {
 			editLink.add(AttributeAppender.append("class", "active"));
 		}
 		
 		container.add(new WebMarkupContainer("canAttachFile").setVisible(getAttachmentSupport()!=null));
 		
-		container.add(ajaxBehavior = new AbstractPostAjaxBehavior() {
+		container.add(actionBehavior = new AbstractPostAjaxBehavior() {
 
 			@Override
 			protected void updateAjaxAttributes(AjaxRequestAttributes attributes) {
 				super.updateAjaxAttributes(attributes);
 				attributes.setChannel(new AjaxChannel("markdown-preview", AjaxChannel.Type.DROP));
@@ -450,17 +458,46 @@ public class MarkdownEditor extends FormComponentPanel<String> {
 						throw new RuntimeException(e);
 					}
 					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);
 				}		
 			}
 			
 		});
+		
+		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
 	protected void onComponentTag(ComponentTag tag) {
 		super.onComponentTag(tag);
@@ -474,35 +511,26 @@ public class MarkdownEditor extends FormComponentPanel<String> {
 	@Override
 	public void renderHead(IHeaderResponse response) {
 		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)
 			autosaveKey = "'" + JavaScriptEscape.escapeJavaScript(autosaveKey) + "'";
 		else
 			autosaveKey = "undefined";
 		
 		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()),
 				autosaveKey);
 		response.render(OnDomReadyHeaderItem.forScript(script));
diff --git a/server-core/src/main/java/io/onedev/server/web/component/markdown/markdown.js b/server-core/src/main/java/io/onedev/server/web/component/markdown/markdown.js
index 34bb1a0ab..83cb6638c 100644
--- a/server-core/src/main/java/io/onedev/server/web/component/markdown/markdown.js
+++ b/server-core/src/main/java/io/onedev/server/web/component/markdown/markdown.js
@@ -12,11 +12,11 @@ onedev.server.markdown = {
 		    var evt = document.createEvent("HTMLEvents");
 		    evt.initEvent("input", false, true);
 		    $input[0].dispatchEvent(evt);
 		}
 	},
-	onDomReady: function(containerId, callback, atWhoLimit, attachmentSupport, 
+	onDomReady: function(containerId, callback, atWhoLimit, attachmentUploadUrl, 
 			attachmentMaxSize, canMentionUser, canReferenceEntity, 
 			projectNamePattern, autosaveKey) {
 		var $container = $("#" + containerId);
 		$container.data("callback", callback);		
 		$container.data("autosaveKey", autosaveKey);
@@ -415,11 +415,11 @@ onedev.server.markdown = {
 		        }, 
 		        limit: atWhoLimit
 		    });		
 	    }
 	    
-	    if (attachmentSupport) {
+	    if (attachmentUploadUrl) {
 	    	var inputEl = $input[0];
 	    	
 			inputEl.addEventListener("paste", function(e) {
 				for (var i = 0; i < e.clipboardData.items.length; i++) {
 					var item = e.clipboardData.items[i];
@@ -480,24 +480,31 @@ onedev.server.markdown = {
 						$input.range(message);
 						$input.caret($input.caret()+message.length);
 					}
 					
 					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);
 				}
 			}
 	    }		
 	},
diff --git a/server-product/src/main/java/io/onedev/server/product/ProductServletConfigurator.java b/server-product/src/main/java/io/onedev/server/product/ProductServletConfigurator.java
index 9f49bba0e..a9648ee4b 100644
--- a/server-product/src/main/java/io/onedev/server/product/ProductServletConfigurator.java
+++ b/server-product/src/main/java/io/onedev/server/product/ProductServletConfigurator.java
@@ -24,11 +24,10 @@ import io.onedev.server.security.DefaultWebEnvironment;
 import io.onedev.server.util.ServerConfig;
 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;
 
 public class ProductServletConfigurator implements ServletConfigurator {
 
@@ -42,30 +41,26 @@ public class ProductServletConfigurator implements ServletConfigurator {
 	
 	private final GitPostReceiveCallback postReceiveServlet;
 	
 	private final WicketServlet wicketServlet;
 	
-	private final AttachmentUploadServlet attachmentUploadServlet;
-	
 	private final ServletContainer jerseyServlet;
 
 	private final WebSocketManager webSocketManager;
 	
 	@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;
 		this.preReceiveServlet = preReceiveServlet;
 		this.postReceiveServlet = postReceiveServlet;
 		this.wicketServlet = wicketServlet;
 		this.webSocketManager = webSocketManager;
 		this.jerseyServlet = jerseyServlet;
-		this.attachmentUploadServlet = attachmentUploadServlet;
 	}
 	
 	@Override
 	public void configure(ServletContextHandler context) {
 		context.setContextPath("/");
@@ -86,12 +81,10 @@ public class ProductServletConfigurator implements ServletConfigurator {
 		 * Add wicket servlet as the default servlet which will serve all requests failed to 
 		 * match a path pattern
 		 */
 		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/*");
 		
 		context.getSessionHandler().addEventListener(new HttpSessionListener() {
 

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) {