One year ago, a website running the Apache Struts web framework in the back and hosted on the Jelastic Cloud and on the provider MIRHosting was subject to a nasty hack.

The hackers were able to take control of our server remotely in order to increase its memory usage to 100 percent so that nobody could access their website. My primitive reaction to that recurring issue was to delete, recreate the environment, reinstall our Apache Tomcat web server, and upload it again to our website with the Apache Maven build tool before the MIRHosting support team told me that they found a perduring suspicious process that was running in the background. I was asked if I started it and if I have an SSH access to someone else. After telling them no, we started an investigation to find the cause of it, and after we read the Apache Tomcat logs, we found that the vulnerability of the Jakarta Multipart parser was being exploited to start the process when we were not using it.

Finding the right solution to the problem was not that hard, since, at that time, I was working on my Rapid Application Development framework, Metamorphosis, built on top of the Apache Struts web framework. In one of my articles, I wrote that, at startup, and if you don't exclude it, a base configuration file named struts-default.xml provided in the struts2-core jar file is automatically included to provide the standard configuration settings without having to copy them, and providing your own custom file is the way to make it more lightweight or to replace the Jakarta Multipart parser. Until today, I had not upgraded to another version, and I'm still using its 2.3.16 version with a very light custom configuration file. And, I'm quite happy with that.

<dependency> <groupId>org.apache.struts</groupId> <artifactId>struts2-core</artifactId> <version>2.3.16</version> </dependency>

package org.metamorphosis.core; import javax.servlet.FilterRegistration; import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.annotation.WebListener; @WebListener public class StartupListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent event) { ServletContext context = event.getServletContext(); FilterRegistration struts2 = context.addFilter("struts2", org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter.class); struts2.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST,DispatcherType.FORWARD),true, "/*"); struts2.setInitParameter("config","struts-custom.xml,struts-plugin.xml,struts.xml"); } @Override public void contextDestroyed(ServletContextEvent event) { } }





To speed up its deployment and load time, our website has been converted to static and hosted on the awesome Netlify platform, months later. Through CORS and with Let's Encrypt, it is the secure entry point (SEP) to subscribe to one of our services identified by a unique name (domainhosting, mailhosting, webdev...) and hosted as generic and lightweight microservices on our Java platform. We did this to later connect to our Java progressive customer portal using your credentials, once your registration has been confirmed and your account activated. This is also the right strategy to avoid the duplication of functionalities among our applications (Portal, CRM...) and our generic use case and the static nature of Java, make the Eclipse MicroProfile project, a solution that we can't use to create our microservices.

Service.xml

<service> <name>name</name> <description>Description of the service</description> </service>





Service.groovy

class Service extends ActionSupport { def subscribe(subscription) { order(subscription.order) } def order(order) { } def pay(bill) { } }





Dispatcher.groovy

class Dispatcher extends ActionSupport { def subscribe() { def subscription = request.body def service = getService(subscription.service) service.subscribe(subscription) json([status:1]) } def order() { def order = request.body def service = getService(order.service) service.order(order) json([status:1]) } def pay() { def bill = request.body def service = getService(bill.service) service.pay(bill) json([status:1]) } }





In the meantime, another RCE vulnerability has been uncovered by security researchers, but fortunately, we are not affected by it since we have wrapped the Apache Struts web framework under a modular and secure layer with our configuration files looking like this for our web applications :

Module.xml

<module> <name>users</name> <url>users</url> <type>back-end</type> <actions> <action url="registration/confirm" method="confirm"/> <action url="login" method="login"/> <action url="account" page="account"/> <action url="account/lock" method="lockAccount"/> <action url="account/unlock" method="unlockAccount"/> <action url="password/change" method="changePassword"/> <action url="password/recover" method="recoverPassword"/> <action url="profile/update" method="updateProfile"/> <action url="collaborators/add" method="addCollaborator"/> <action url="collaborators/invite" method="inviteCollaborator"/> <action url="collaborators/remove" method="removeCollaborator"/> <action url="collaborators/info" method="getCollaboratorInfo"/> <action url="logout" method="logout"/> </actions> </module>





At startup, the Apache Struts configuration files are generated from ours and on development. Whenever the module.xml configuration file of a module is updated, the configuration of its package is automatically reloaded using builders, so we don't have to restart our Apache Tomcat web server in order to have the changes applied. The result types of an Action can only be tiles, redirect, or dispatcher. The other types are discarded and with the default ActionSupport class overloaded. One can simply choose to redirect or forward a request within an Action with the corresponding method, like this :

Module.groovy

class ModuleAction extends ActionSupport { def logout() { session.invalidate() redirect(contextPath) } }





To send a malicious HTTP request with an OGNL expression in the Uniform Resource Identifier query, you will only cause a redirection to the root of the web application by our interceptor since the URI does not map to the URL of a module. As you can read it below, the Jakarta Multipart parser has been replaced in our system by our custom class and the FileUploadInterceptor class, which is included as part of the default stack has been removed from our custom configuration file.

Struts-custom.xml

<struts> <bean type="org.apache.struts2.dispatcher.multipart.MultiPartRequest" name="jakarta" class="org.metamorphosis.core.MultiPartRequest" scope="prototype"/> <package name="struts-default" abstract="true"> <result-types> <result-type name="dispatcher" class="org.apache.struts2.dispatcher.ServletDispatcherResult" default="true"/> <result-type name="redirect" class="org.apache.struts2.dispatcher.ServletRedirectResult"/> </result-types> <interceptors> <interceptor name="exception" class="com.opensymphony.xwork2.interceptor.ExceptionMappingInterceptor"/> <interceptor-stack name="defaultStack"> <interceptor-ref name="exception"/> </interceptor-stack> </interceptors> <default-interceptor-ref name="defaultStack"/> <default-class-ref class="org.metamorphosis.core.ActionSupport" /> </package> </struts>





MultiPartRequest.java

package org.metamorphosis.core; import java.io.File; import java.io.IOException; import java.util.Enumeration; import java.util.List; import javax.servlet.http.HttpServletRequest; public class MultiPartRequest implements org.apache.struts2.dispatcher.multipart.MultiPartRequest { @Override public void parse(HttpServletRequest request, String saveDir) throws IOException { } @Override public Enumeration<String> getFileParameterNames() { return null; } @Override public String[] getContentType(String fieldName) { return null; } @Override public File[] getFile(String fieldName) { return null; } @Override public String[] getFileNames(String fieldName) { return null; } @Override public String[] getFilesystemName(String fieldName) { return null; } @Override public String getParameter(String name) { return null; } @Override public Enumeration<String> getParameterNames() { return null; } @Override public String[] getParameterValues(String name) { return null; } @Override public List<String> getErrors() { return new ArrayList<String>(); } @Override public void cleanUp() { } }





Replacing the Jakarta Multipart parser with a class does nothing. It is also the right way for us to get rid of the awful Apache Struts file upload design when all the uploaded files are saved to a temporary directory by the framework before being passed in to an Action. This approach using the traditional API, which is described in the User Guide, is totally inefficient, memory and time consuming, and particulary, when the files must be stored on a file storage service like Dropbox using its Java API and the use of variables to insert the uploaded files and related data into an Action, makes this one to be stateful.

The right approach to managing the upload is to use a servlet in combination with the Apache Commons FileUpload streaming API to store the files on the fly, on an external repository, for an optimal performance and a low memory profile. Additionally, the streaming API is more lightweight and, thus, is easier to understand than the complicated Spring file upload mechanism.

UploadServlet.java

package app; import java.io.File; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.fileupload.FileItemIterator; import org.apache.commons.fileupload.FileItemStream; import org.apache.commons.fileupload.servlet.ServletFileUpload; @WebServlet("/documents/upload.html") public class UploadServlet extends HttpServlet { @Override public void doPost(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException { if(ServletFileUpload.isMultipartContent(request)) { try { FileManager manager = new FileManager("documents"); FileItemIterator it = new ServletFileUpload().getItemIterator(request); while(it.hasNext()) { FileItemStream item = it.next(); manager.upload(new File(item.getName()).getName(),item.openStream()); } }catch(Exception e) { } } response.getWriter().write("{\"status\" : 1}"); } }





FileManager.java

package app; import java.io.InputStream; import java.io.OutputStream; import com.dropbox.core.DbxRequestConfig; import com.dropbox.core.v2.DbxClientV2; import com.dropbox.core.v2.files.WriteMode; public class FileManager { private final String folder; private final DbxClientV2 client; public FileManager(String folder) { this.folder = folder; client = new DbxClientV2(new DbxRequestConfig("dropbox/myapp"),System.getenv("dropbox_key")); } public void upload(String name,InputStream in) throws Exception { client.files().uploadBuilder("/"+folder+"/"+name).withMode(WriteMode.OVERWRITE).uploadAndFinish(in); } public void download(String name,OutputStream out) throws Exception { client.files().downloadBuilder("/"+folder+"/"+name).start().download(out); } }





Using the JQuery Ajax function, our JavaScript client-side code looks like this on a form submission:

page.uploadDocuments = function(form) { $.ajax({ type: "POST", enctype: 'multipart/form-data', url: form.attr("action"), data: new FormData(form[0]), contentType : false, cache: false, processData:false, success: function(response) { }, error : function() { }, dataType : "json" }); };





Our HTML form has at least one file required for a submission and, of course, the name given to an input field is not the one used when the corresponding uploaded file is saved on the server-side.

<form action="documents/upload.html"> <fieldset> <span> Document 1 </span> <input name="file1" type="file" required> <span> Document 2 </span> <input name="file2" type="file"> <span> Document 3 </span> <input name="file3" type="file"> </fieldset> <div class="submit"> <input type="submit" value="Send"> <input type="button" value="Cancel"> </div> </form>



