CVE-2021-32634 (published 2021-05-21T18:15Z)

Emissary is a distributed, peer-to-peer, data-driven workflow framework. Emissary 6.4.0 is vulnerable to Unsafe Deserialization of post-authenticated requests to the [`WorkSpaceClientEnqueue.action`](https://github.com/NationalSecurityAgency/emissary/blob/30c54ef16c6eb6ed09604a929939fb9f66868382/src/main/java/emissary/server/mvc/internal/WorkSpaceClientEnqueueAction.java) REST endpoint. This issue may lead to post-auth Remote Code Execution. This issue has been patched in version 6.5.0. As a workaround, one can disable network access to Emissary from untrusted sources.

Severity: MEDIUM
CVSS v2 base score: 6.5 (AV:N/AC:L/Au:S/C:P/I:P/A:P)
CVSS v3 base score: 7.2, HIGH (CVSS:3.1/AV:N/AC:L/PR:H/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/NationalSecurityAgency/emissary
commit_details: repo_url: https://github.com/NationalSecurityAgency/emissary
author: Drew Farris
author_date: 2021-05-21 10:32:31-04:00
msg: Merge pull request from GHSA-m5qf-gfmp-7638

* Remove unsafe serialization from PayloadUtil

* This code will likely be removed wholesale, but this change
  should be used as a departure point for future development
  if we end up re-implementing moveTo and friends.

* Removed vestigial MoveTo related code.

* Remove unsafe serialization in WorkSpace infra.

* Favor DataInput/DataOutputStream over ObjectInput/ObjectOutputStream
* Implement lightweight serialization in WorkBundle/WorkUnit

* Updates to WorkBundle serDe, added tests.

- set limit on number of WorkUnits per bundle. In practice these are
  commonly less than 1024.
- added null handling for WorkBundle/WorkUnit string fields.
- confirmed readUTF/writeUTF has a limit ensuring strings will
  be 65535 characters or less.

* Minor cleanup to WorkBundleTest

* Minor Change to WorkBundleTest

* Formatting updates
repository: repo_url: https://github.com/NationalSecurityAgency/emissary
repo_name: NationalSecurityAgency/emissary
description: Distributed P2P Data-driven Workflow Framework
repo_language: Java
owner: NationalSecurityAgency
file_changes: filename: IMobileAgent.java
change_type: ModificationType.MODIFY
diff: @@ -3,8 +3,6 @@ package emissary.core;
 import java.io.Serializable;
 import java.util.List;
 
-import emissary.server.mvc.adapters.MoveToAdapter;
-
 /**
  * Interface to the MobileAgent
  */
@@ -59,9 +57,8 @@ public interface IMobileAgent extends Serializable, Runnable {
     boolean isInUse();
 
     /**
-     * Get the payload as an object for serialization during transport Should only be called by the MoveToAdapter
-     * 
-     * @see MoveToAdapter
+     * Get the payload as an object for serialization during transport.
+     *
      */
     Object getPayloadForTransport();
 

programming_language: Java
filename: WorkBundle.java
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,8 @@
 package emissary.pickup;
 
-import java.io.Serializable;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
@@ -21,13 +23,12 @@ import org.slf4j.LoggerFactory;
  * <p>
  * getOldestFileModificationTime() &lt;= getYoungestFileModificationTime()
  */
-public class WorkBundle implements Serializable, Comparable<WorkBundle> {
-
-    // Serializable
-    static final long serialVersionUID = 6339812801001572532L;
+public final class WorkBundle implements Comparable<WorkBundle> {
 
     private static final Logger logger = LoggerFactory.getLogger(WorkBundle.class);
 
+    static final int MAX_UNITS = 1024;
+
     // Unique ID for this work bundle
     String bundleId;
 
@@ -110,6 +111,80 @@ public class WorkBundle implements Serializable, Comparable<WorkBundle> {
         resetBundleId();
     }
 
+    /**
+     * Deserialize a WorkBundle from a DataInputStream
+     *
+     * @param in the stream to read from
+     * @return the deserialized WorkBundle
+     * @throws IOException if there is a problem reading the stream or it contains more than <code>MAX_UNITS</code> work
+     *         units.
+     */
+    public static WorkBundle readFromStream(DataInputStream in) throws IOException {
+        WorkBundle wb = new WorkBundle();
+        wb.bundleId = readUTFOrNull(in);
+        wb.outputRoot = readUTFOrNull(in);
+        wb.eatPrefix = readUTFOrNull(in);
+        wb.caseId = readUTFOrNull(in);
+        wb.sentTo = readUTFOrNull(in);
+        wb.errorCount = in.readInt();
+        wb.priority = in.readInt();
+        wb.simpleMode = in.readBoolean();
+        wb.oldestFileModificationTime = in.readLong();
+        wb.youngestFileModificationTime = in.readLong();
+        wb.totalFileSize = in.readLong();
+        int workUnitSize = in.readInt();
+        if (workUnitSize > MAX_UNITS) {
+            throw new IOException(
+                    "Exception when reading: WorkBundle may not contain more then " + MAX_UNITS + " WorkUnits (saw: " + workUnitSize + ").");
+        }
+        for (int i = 0; i < workUnitSize; i++) {
+            wb.addWorkUnit(WorkUnit.readFromStream(in));
+        }
+        return wb;
+    }
+
+    /**
+     * Serialize this WorkBundle to a DataOutputStream
+     *
+     * @param out the stream to write to.
+     * @throws IOException if there is a problem writing to the stream.
+     */
+    public void writeToStream(DataOutputStream out) throws IOException {
+        writeUTFOrNull(bundleId, out);
+        writeUTFOrNull(outputRoot, out);
+        writeUTFOrNull(eatPrefix, out);
+        writeUTFOrNull(caseId, out);
+        writeUTFOrNull(sentTo, out);
+        out.writeInt(errorCount);
+        out.writeInt(priority);
+        out.writeBoolean(simpleMode);
+        out.writeLong(oldestFileModificationTime);
+        out.writeLong(youngestFileModificationTime);
+        out.writeLong(totalFileSize);
+        out.writeInt(workUnitList.size());
+        if (workUnitList.size() > MAX_UNITS) {
+            throw new IOException(
+                    "Exception when writing: WorkBundle may not contain more then " + MAX_UNITS + " WorkUnits (saw: " + workUnitList.size() + ").");
+        }
+        for (WorkUnit u : workUnitList) {
+            u.writeToStream(out);
+        }
+    }
+
+    static String readUTFOrNull(DataInputStream in) throws IOException {
+        if (in.readBoolean()) {
+            return in.readUTF();
+        }
+        return null;
+    }
+
+    static void writeUTFOrNull(String s, DataOutputStream out) throws IOException {
+        out.writeBoolean(s != null);
+        if (s != null) {
+            out.writeUTF(s);
+        }
+    }
+
     /**
      * Set the work bundle id
      * 
@@ -204,8 +279,13 @@ public class WorkBundle implements Serializable, Comparable<WorkBundle> {
      *
      * @param workUnit the workUnit to add
      * @return number of WorkUnits in list after add
+     * @throws IllegalStateException if adding the unit would cause the bundle to contain more than <code>MAX_UNITS</code>
+     *         work units
      */
     public int addWorkUnit(WorkUnit workUnit) {
+        if (workUnitList.size() >= MAX_UNITS) {
+            throw new IllegalStateException("WorkBundle may not contain more than " + MAX_UNITS + " WorkUnits.");
+        }
         workUnitList.add(workUnit);
         return size();
     }
@@ -215,10 +295,14 @@ public class WorkBundle implements Serializable, Comparable<WorkBundle> {
      * 
      * @param workUnit the workUnit to add
      * @param fileModificationTimeInMillis the file modification time in milliseconds since epoch
+     * @param fileSize the size of the file added.
+     * @throws IllegalStateException if adding the unit would cause the bundle to contain more than <code>MAX_UNITS</code>
+     *         work units
      * @return number of files in this set after update
      */
     public int addWorkUnit(WorkUnit workUnit, long fileModificationTimeInMillis, long fileSize) {
-        workUnitList.add(workUnit);
+        addWorkUnit(workUnit);
+
         if (fileModificationTimeInMillis < oldestFileModificationTime) {
             oldestFileModificationTime = fileModificationTimeInMillis;
         }
@@ -231,8 +315,16 @@ public class WorkBundle implements Serializable, Comparable<WorkBundle> {
 
     /**
      * Add from a list, without adjusting file modification time tracking.
+     * 
+     * @param list a list of WorkUnits to add to this bundle
+     * @return the total size of WorkUnits in this bundle
+     * @throws IllegalStateException if adding the units would cause the bundle to contain more than <code>MAX_UNITS</code>
+     *         work units
      */
     protected int addWorkUnits(List<WorkUnit> list) { // This appears to only be used by unit tests and the copy constructor
+        if (workUnitList.size() + list.size() > MAX_UNITS) {
+            throw new IllegalStateException("WorkBundle may not contain more than " + MAX_UNITS + " WorkUnits.");
+        }
         workUnitList.addAll(list);
         return workUnitList.size();
     }
@@ -265,10 +357,11 @@ public class WorkBundle implements Serializable, Comparable<WorkBundle> {
      * 
      * @param file string file name consistent with outputRoot
      * @return number of files in this set after update
+     * @throws IllegalStateException if adding the file would cause the bundle to contain more than <code>MAX_UNITS</code>
+     *         work units
      */
     public int addFileName(String file) {
-        workUnitList.add(new WorkUnit(file));
-        return size();
+        return addWorkUnit(new WorkUnit(file));
     }
 
     /**
@@ -276,7 +369,10 @@ public class WorkBundle implements Serializable, Comparable<WorkBundle> {
      * 
      * @param file string file name consistent with outputRoot
      * @param fileModificationTimeInMillis the file modification time in milliseconds since epoch
+     * @param fileSize the size of the file being added
      * @return number of files in this set after update
+     * @throws IllegalStateException if adding the file would cause the bundle to contain more than <code>MAX_UNITS</code>
+     *         work units
      */
     public int addFileName(String file, long fileModificationTimeInMillis, long fileSize) {
         return addWorkUnit(new WorkUnit(file), fileModificationTimeInMillis, fileSize);
@@ -287,21 +383,27 @@ public class WorkBundle implements Serializable, Comparable<WorkBundle> {
      * 
      * @param file string file names consistent with outputRoot
      * @return number of files in this set after update
+     * @throws IllegalStateException if adding the files would cause the bundle to contain more than <code>MAX_UNITS</code>
+     *         work units
      */
     protected int addFileNames(String[] file) { // This appears to only be used by unit tests
-        for (int i = 0; file != null && i < file.length; i++) {
-            workUnitList.add(new WorkUnit(file[i]));
+        for (String f : file) {
+            addWorkUnit(new WorkUnit(f));
         }
         return size();
     }
 
     /**
      * Add from a list, without adjusting file modification time tracking.
+     * 
+     * @param list the list of files to add
+     * @throws IllegalStateException if adding the files would cause the bundle to contain more than <code>MAX_UNITS</code>
+     *         work units
      */
     protected int addFileNames(List<String> list) { // This appears to only be used by unit tests and the copy
                                                     // constructor
         for (String file : list) {
-            workUnitList.add(new WorkUnit(file));
+            addWorkUnit(new WorkUnit(file));
         }
         return size();
     }

programming_language: Java
filename: WorkUnit.java
change_type: ModificationType.MODIFY
diff: @@ -1,10 +1,14 @@
 package emissary.pickup;
 
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
 /**
  * A WorkUnit is a unit of work a worker will process. The idea is to replace fileNameList. Currently, WorkBundle is set
  * to only have one file, and so there will only be one WorkUnit.
  */
-public class WorkUnit {
+public final class WorkUnit {
     private String fileName;
     private String transactionId;
     // worker updates this boolean
@@ -36,6 +40,22 @@ public class WorkUnit {
         this.failedToProcess = failedToProcess;
     }
 
+    public static WorkUnit readFromStream(DataInputStream in) throws IOException {
+        final WorkUnit u = new WorkUnit(null);
+        u.fileName = WorkBundle.readUTFOrNull(in);
+        u.transactionId = WorkBundle.readUTFOrNull(in);
+        u.failedToParse = in.readBoolean();
+        u.failedToProcess = in.readBoolean();
+        return u;
+    }
+
+    public void writeToStream(DataOutputStream out) throws IOException {
+        WorkBundle.writeUTFOrNull(fileName, out);
+        WorkBundle.writeUTFOrNull(transactionId, out);
+        out.writeBoolean(failedToParse);
+        out.writeBoolean(failedToProcess);
+    }
+
     /**
      * Gets the filename for the WorkUnit
      * 

programming_language: Java
filename: MoveToAdapter.java
change_type: ModificationType.DELETE
diff: @@ -1,267 +0,0 @@
-package emissary.server.mvc.adapters;
-
-import java.io.IOException;
-import java.nio.charset.Charset;
-import java.rmi.RemoteException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.ws.rs.core.MediaType;
-
-import emissary.client.EmissaryClient;
-import emissary.client.EmissaryResponse;
-import emissary.config.ConfigUtil;
-import emissary.config.Configurator;
-import emissary.core.EmissaryException;
-import emissary.core.IMobileAgent;
-import emissary.core.Namespace;
-import emissary.core.NamespaceException;
-import emissary.directory.DirectoryEntry;
-import emissary.directory.KeyManipulator;
-import emissary.log.MDCConstants;
-import emissary.place.IServiceProviderPlace;
-import emissary.pool.AgentPool;
-import emissary.util.PayloadUtil;
-import org.apache.http.HttpStatus;
-import org.apache.http.HttpVersion;
-import org.apache.http.NameValuePair;
-import org.apache.http.client.entity.EntityBuilder;
-import org.apache.http.client.entity.UrlEncodedFormEntity;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.impl.cookie.BasicClientCookie;
-import org.apache.http.message.BasicHttpResponse;
-import org.apache.http.message.BasicNameValuePair;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.slf4j.MDC;
-
-/**
- * Facilitate moving agents over HTTP
- */
-public class MoveToAdapter extends EmissaryClient {
-
-    private static final Logger logger = LoggerFactory.getLogger(MoveToAdapter.class);
-
-    public static final String PLACE_NAME = "mtPlaceName";
-    public static final String AGENT_OBJECT = "mtAgentObject";
-    public static final String AGENT_SERIAL = "mtAgentSerialized";
-    public static final String MOVE_ERROR_COUNT = "agentMoveErrorCount";
-    public static final String ITINERARY_ITEM = "agentItineraryItem";
-
-    private static String COOKIE_NAME = "PLACE";
-    private static String COOKIE_DOMAIN = "www.example.com";
-    private static String COOKIE_PATH = "/";
-    private static String VIRTUAL_MOVETO_PROTOCOL = "http";
-    private static String VIRTUAL_MOVETO_ADDR = null;
-
-    // Initialize the VIRTUAL_MOVETO_ADDR IP and port once on class load
-    static {
-        reconfigure();
-    }
-
-    /**
-     * Configure static stuff for this class Config items read here are
-     * 
-     * <ul>
-     * <li>VIRTUAL_MOVETO_ADDR: default null for redirecting moveTo calls when dynamic loadbalancing is available</li>
-     * <li>VIRTUAL_MOVETO_PROTOCOL: default http protocol for loadbalancing redirect request</li>
-     * <li>VIRTUAL_COOKIE_NAME: name of coookie holding real destination place, default=PLACE</li>
-     * <li>VIRTUAL_COOKIE_DOMAIN: name of cookie domain for redirect cookie, default=www.example.com</li>
-     * <li>VIRTUAL_COOKIE_PATH: path set on redirect cookie, default=/</li>
-     * </ul>
-     */
-    public static void reconfigure() {
-        try {
-            final Configurator conf = ConfigUtil.getConfigInfo(AgentPool.class);
-            VIRTUAL_MOVETO_ADDR = conf.findStringEntry("VIRTUAL_MOVETO_ADDR", null);
-            VIRTUAL_MOVETO_PROTOCOL = conf.findStringEntry("VIRTUAL_MOVETO_PROTOCOL", "http");
-            COOKIE_NAME = conf.findStringEntry("VIRTUAL_COOKIE_NAME", COOKIE_NAME);
-            COOKIE_DOMAIN = conf.findStringEntry("VIRTUAL_COOKIE_DOMAIN", COOKIE_DOMAIN);
-            COOKIE_PATH = conf.findStringEntry("VIRTUAL_COOKIE_PATH", COOKIE_PATH);
-        } catch (IOException e) {
-            logger.warn("Cannot read config file, virtual hosting capability not available " + e.getMessage());
-        }
-    }
-
-    /**
-     * Public constructor
-     */
-    public MoveToAdapter() {}
-
-    /**
-     * moveTo call arriving on this server, translate to real call
-     * 
-     * @param req the HttpRequest with all needed parameters
-     */
-    public boolean inboundMoveTo(final HttpServletRequest req) throws RemoteException, NamespaceException {
-
-        final MoveToRequestBean bean = new MoveToRequestBean(req);
-        String placeKey = KeyManipulator.getServiceLocation(bean.getPlaceName());
-
-        // Rewrite the placeKey to ignore the machine:port part of the key
-        // since we may have come here on a redirect from the load
-        // balancing module and it may not be the right host name anymore
-        if (VIRTUAL_MOVETO_ADDR != null) {
-            placeKey = KeyManipulator.getServiceClassname(bean.getPlaceName());
-        }
-
-        final IServiceProviderPlace place = (IServiceProviderPlace) Namespace.lookup(placeKey);
-        if (place == null) {
-            throw new NamespaceException("Nothing found for " + bean.getPlaceName() + " using " + placeKey + " as the lookup key");
-        }
-
-        try {
-            emissary.pool.PayloadLauncher.launch(bean.getPayload(), place, bean.getErrorCount(), bean.getItineraryItems());
-        } catch (EmissaryException ex) {
-            logger.debug("Cannot launch incoming payload", ex);
-            throw new RemoteException("Cannot launch payload", ex);
-        } finally {
-            MDC.remove(MDCConstants.SERVICE_LOCATION);
-        }
-
-        return true;
-    }
-
-    /**
-     * Send a moveTo call to a remote machine
-     * 
-     * @param place the four-tuple of the place we are heading to
-     * @param agent the MobileAgent that is moving
-     * @return status of operation including body if successful
-     */
-    public EmissaryResponse outboundMoveTo(final String place, final IMobileAgent agent) {
-
-        String url = null;
-
-        // Move to actions can be load-balanced out to
-        // a virtual IP address:port if so configured
-        if (VIRTUAL_MOVETO_ADDR != null) {
-            url = VIRTUAL_MOVETO_PROTOCOL + "://" + VIRTUAL_MOVETO_ADDR + "/";
-        } else {
-            url = KeyManipulator.getServiceHostURL(place);
-        }
-        url += CONTEXT + "/MoveTo.action";
-
-        final HttpPost method = createHttpPost(url, CONTEXT, "/MoveTo.action");
-        method.setHeader("Content-type", "application/x-www-form-urlencoded; charset=ISO-8859-1");
-        final List<NameValuePair> nvps = new ArrayList<NameValuePair>();
-
-        nvps.add(new BasicNameValuePair(PLACE_NAME, place));
-        nvps.add(new BasicNameValuePair(MOVE_ERROR_COUNT, Integer.toString(agent.getMoveErrorCount())));
-
-        final DirectoryEntry[] iq = agent.getItineraryQueueItems();
-        for (int j = 0; j < iq.length; j++) {
-            nvps.add(new BasicNameValuePair(ITINERARY_ITEM, iq[j].getKey()));
-        }
-
-        try {
-            // This is an 8859_1 String
-            final String agentData = PayloadUtil.serializeToString(agent.getPayloadForTransport());
-            nvps.add(new BasicNameValuePair(AGENT_SERIAL, agentData));
-        } catch (IOException iox) {
-            // TODO This will probably need looked at when redoing the moveTo
-            logger.error("Cannot serialize agent data", iox);
-            BasicHttpResponse response =
-                    new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_INTERNAL_SERVER_ERROR, "Cannot serialize agent data");
-            response.setEntity(EntityBuilder.create().setText("").setContentEncoding(MediaType.TEXT_PLAIN).build());
-            return new EmissaryResponse(response);
-        }
-
-        method.setEntity(new UrlEncodedFormEntity(nvps, Charset.forName("8859_1")));
-
-        // Add a cookie to the outbound header if we are posting
-        // to the virtual IP for load balancing
-        if (VIRTUAL_MOVETO_ADDR != null) {
-            final BasicClientCookie cookie = new BasicClientCookie(COOKIE_NAME, KeyManipulator.getServiceClassname(place));
-            cookie.setDomain(VIRTUAL_MOVETO_ADDR.substring(0, VIRTUAL_MOVETO_ADDR.indexOf(":")));
-            cookie.setPath(COOKIE_PATH);
-            return send(method, cookie);
-        }
-        return send(method);
-    }
-
-
-    static class MoveToRequestBean {
-        String placeName;
-        Object payload;
-        int errorCount;
-        List<DirectoryEntry> itineraryItems = null;
-
-        MoveToRequestBean() {}
-
-        MoveToRequestBean(final HttpServletRequest req) {
-            setPlaceName(RequestUtil.getParameter(req, PLACE_NAME));
-            if (getPlaceName() == null) {
-                throw new IllegalArgumentException("Missing place name");
-            }
-
-            final String agentData = RequestUtil.getParameter(req, AGENT_SERIAL);
-            if (agentData == null) {
-                throw new IllegalArgumentException("Missing serialized agent data");
-            }
-            setPayload(agentData);
-
-            setErrorCount(RequestUtil.getIntParam(req, MOVE_ERROR_COUNT, 0));
-
-            final String[] p = req.getParameterValues(ITINERARY_ITEM);
-            if (p != null && p.length > 0) {
-                this.itineraryItems = new ArrayList<DirectoryEntry>();
-                for (int i = 0; i < p.length; i++) {
-                    this.itineraryItems.add(new DirectoryEntry(p[i]));
-                }
-            }
-        }
-
-        void setPayload(final String s) {
-            this.payload = PayloadUtil.deserialize(s);
-        }
-
-        /**
-         * Get the place name for arrival
-         */
-        String getPlaceName() {
-            return this.placeName;
-        }
-
-        /**
-         * Set the place name
-         * 
-         * @param argPlaceName value of placeName
-         */
-        void setPlaceName(final String argPlaceName) {
-            this.placeName = argPlaceName;
-        }
-
-        /**
-         * Get the agent's payload data. The actual type will depend on the particular implementation of IMobileAgent that
-         * serialized the data to us.
-         */
-        Object getPayload() {
-            return this.payload;
-        }
-
-        void setErrorCount(final int c) {
-            this.errorCount = c;
-        }
-
-        int getErrorCount() {
-            return this.errorCount;
-        }
-
-        int getItineraryItemCount() {
-            return this.itineraryItems == null ? 0 : this.itineraryItems.size();
-        }
-
-        /**
-         * Return the transferred itinerary items as a list
-         * 
-         * @return list of DirectoryEntry
-         */
-        @SuppressWarnings("unchecked")
-        List<DirectoryEntry> getItineraryItems() {
-            return this.itineraryItems == null ? Collections.EMPTY_LIST : this.itineraryItems;
-        }
-    }
-}

programming_language: unknown
filename: WorkSpaceAdapter.java
change_type: ModificationType.MODIFY
diff: @@ -2,7 +2,7 @@ package emissary.server.mvc.adapters;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
-import java.io.ObjectInputStream;
+import java.io.DataInputStream;
 import java.io.ObjectOutputStream;
 import java.io.UnsupportedEncodingException;
 import java.util.ArrayList;
@@ -301,8 +301,8 @@ public class WorkSpaceAdapter extends EmissaryClient {
          */
         void setPaths(final String s) {
             try {
-                final ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(s.getBytes("8859_1")));
-                this.paths = (WorkBundle) ois.readObject();
+                final DataInputStream dis = new DataInputStream(new ByteArrayInputStream(s.getBytes("8859_1")));
+                this.paths = WorkBundle.readFromStream(dis);
             } catch (Exception e) {
                 logger.error("Cannot deserialize WorkBundle using {} bytes", s.length(), e);
                 throw new IllegalArgumentException("Cannot deserialize WorkBundle");

programming_language: Java
filename: MoveToAction.java
change_type: ModificationType.DELETE
diff: @@ -1,44 +0,0 @@
-package emissary.server.mvc.internal;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.Response;
-
-import emissary.server.mvc.adapters.MoveToAdapter;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class MoveToAction {
-    private final Logger logger = LoggerFactory.getLogger(this.getClass());
-
-    /*
-     * <!-- Cause an agent to move to the remote machine/place --> <Use-Case source="*" action="/MoveTo.action"> <Work
-     * type="Bean" target="emissary.comms.http.worker.LogWorker"/> <Work type="Bean"
-     * target="emissary.comms.http.worker.ArrivalWorker"/> <View status="0" view="/success.jsp"/> <View status="-1"
-     * view="/error.jsp"/> </Use-Case>
-     */
-
-    // TODO This is an initial crack at the new endpoint, I haven't seen it called an am unsure when/if it does
-    public Response moveTo(@Context HttpServletRequest request) {
-        // This is copy and pasted from the old ArrivalWorker class
-        final MoveToAdapter mt = new MoveToAdapter();
-        try {
-            final boolean status = mt.inboundMoveTo(request);
-
-            if (!status) {
-                logger.error("MoveTo failed!");
-                // return new WorkerStatus(WorkerStatus.FAILURE, "MoveTo failed");
-                return Response.serverError().entity("MoveTo failed").build();
-            }
-        } catch (Exception e) {
-            logger.error("Could not call moveTo", e);
-            // return new WorkerStatus(WorkerStatus.FAILURE, "Could not call moveTo", e);
-            return Response.serverError().entity("Could not call moveTo").build();
-        }
-
-        logger.debug("MoveTo succeeded");
-        // return WORKER_SUCCESS;
-        return Response.ok().entity("MoveTo succeeded").build();
-    }
-
-}

programming_language: unknown
filename: WorkSpaceClientEnqueueAction.java
change_type: ModificationType.MODIFY
diff: @@ -1,7 +1,7 @@
 package emissary.server.mvc.internal;
 
 import java.io.ByteArrayInputStream;
-import java.io.ObjectInputStream;
+import java.io.DataInputStream;
 
 import javax.ws.rs.Consumes;
 import javax.ws.rs.FormParam;
@@ -49,8 +49,8 @@ public class WorkSpaceClientEnqueueAction {
                 throw new IllegalArgumentException("No client place found using name " + clientName);
             }
 
-            final ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(workBundleString.getBytes("8859_1")));
-            WorkBundle paths = (WorkBundle) ois.readObject();
+            final DataInputStream ois = new DataInputStream(new ByteArrayInputStream(workBundleString.getBytes("8859_1")));
+            WorkBundle paths = WorkBundle.readFromStream(ois);
             success = place.enque(paths);
         } catch (Exception e) {
             logger.warn("WorkSpaceClientEnqueWorker exception", e);

programming_language: Java
filename: PayloadUtil.java
change_type: ModificationType.MODIFY
diff: @@ -1,13 +1,5 @@
 package emissary.util;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.io.OutputStream;
-import java.io.UnsupportedEncodingException;
-import java.nio.ByteBuffer;
 import java.util.Collection;
 import java.util.Date;
 import java.util.Iterator;
@@ -109,73 +101,6 @@ public class PayloadUtil {
         return sb.toString();
     }
 
-    public static ByteBuffer serializeToByteBuffer(final Object payload) throws IOException {
-        return ByteBuffer.wrap(serializeToBytes(payload));
-    }
-
-    /**
-     * Serialize a payload object to bytes
-     */
-    public static byte[] serializeToBytes(final Object payload) throws IOException {
-        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
-        serializeToStream(bos, payload);
-        return bos.toByteArray();
-    }
-
-    /**
-     * Serialize a payload object to string
-     */
-    public static String serializeToString(final Object payload) throws IOException {
-        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
-        serializeToStream(bos, payload);
-        String agentData = null;
-        try {
-            agentData = bos.toString("8859_1");
-        } catch (UnsupportedEncodingException e) {
-            logger.warn("Should always support 8859_1", e);
-            agentData = bos.toString();
-        }
-        return agentData;
-    }
-
-    /**
-     * Serialize a payload object onto the specified stream
-     */
-    public static void serializeToStream(final OutputStream os, final Object payload) throws IOException {
-        ObjectOutputStream oos = null;
-        try {
-            oos = new ObjectOutputStream(os);
-            oos.writeObject(payload);
-        } finally {
-            if (oos != null) {
-                try {
-                    oos.close();
-                } catch (IOException ignore) {
-                    // empty catch block
-                }
-            }
-        }
-    }
-
-    public static Object deserialize(final String s) {
-        ObjectInputStream ois = null;
-        try {
-            ois = new ObjectInputStream(new ByteArrayInputStream(s.getBytes("8859_1")));
-            return ois.readObject();
-        } catch (Exception e) {
-            logger.error("Cannot deserialize payload using " + (s == null ? -1 : s.length()) + " bytes", e);
-            throw new IllegalArgumentException("Cannot deserialize payload");
-        } finally {
-            if (ois != null) {
-                try {
-                    ois.close();
-                } catch (IOException ignore) {
-                    // empty catch block
-                }
-            }
-        }
-    }
-
     /**
      * Turn the payload into an xml jdom document
      * 
@@ -203,7 +128,7 @@ public class PayloadUtil {
         final Element meta = new Element("metadata");
         for (final String key : d.getParameters().keySet()) {
             final Element m = JDOMUtil.protectedElement("param", d.getStringParameter(key));
-            m.setAttribute("name", key.toString());
+            m.setAttribute("name", key);
             meta.addContent(m);
         }
         root.addContent(meta);
@@ -230,8 +155,7 @@ public class PayloadUtil {
         }
 
         logger.debug("Produced xml document for " + d.shortName());
-        final Document doc = new Document(root);
-        return doc;
+        return new Document(root);
     }
 
     /**
@@ -255,8 +179,7 @@ public class PayloadUtil {
             root.addContent(doc.detachRootElement());
             logger.debug("Adding xml content for " + d.shortName() + " to document");
         }
-        final Document doc = new Document(root);
-        return doc;
+        return new Document(root);
     }
 
     /**
@@ -277,7 +200,7 @@ public class PayloadUtil {
         final StringBuilder out = new StringBuilder();
         out.append(LS);
         for (final Map.Entry<String, Collection<Object>> entry : payload.getParameters().entrySet()) {
-            out.append(entry.getKey() + SEP + entry.getValue() + LS);
+            out.append(entry.getKey()).append(SEP).append(entry.getValue()).append(LS);
         }
         return out.toString();
     }

programming_language: Java
filename: WorkBundleTest.java
change_type: ModificationType.MODIFY
diff: @@ -7,6 +7,12 @@ import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
@@ -233,4 +239,100 @@ public class WorkBundleTest extends UnitTest {
         assertEquals(7L, w.getOldestFileModificationTime());
     }
 
+    @Test
+    public void testSerDe() throws IOException {
+        WorkBundle w1 = new WorkBundle("/output/root", "/etc/prefix");
+        w1.addFileName("file1.txt", 15L, 4L);
+        w1.addFileName("<file2.txt&foo=bar>", 7L, 10L);
+
+        ByteArrayOutputStream bout = new ByteArrayOutputStream();
+        DataOutputStream out = new DataOutputStream(bout);
+        w1.writeToStream(out);
+        out.close();
+        bout.close();
+
+        byte[] b = bout.toByteArray();
+
+        ByteArrayInputStream bin = new ByteArrayInputStream(b);
+        DataInputStream in = new DataInputStream(bin);
+        WorkBundle w2 = WorkBundle.readFromStream(in);
+
+        assertEquals(0, w1.compareTo(w2));
+    }
+
+    @Test
+    public void testLimitAdd() throws IOException {
+        // generate test data.
+        final List<WorkUnit> wul = new ArrayList<>();
+        for (int i = 0; i < WorkBundle.MAX_UNITS + 2; i++) {
+            String fileName = UUID.randomUUID().toString();
+            wul.add(new WorkUnit(fileName));
+        }
+
+        // test add list of WorkUnits
+        try {
+            WorkBundle wb = new WorkBundle();
+            wb.addWorkUnits(wul);
+            fail("Did not catch expected exception when adding work unit list.");
+        } catch (IllegalStateException e) {
+            // this is the expected outcome.
+        } catch (Throwable t) {
+            fail("Unexpected Exception caught when adding work unit list: " + t);
+
+        }
+
+        // test add individual WorkUnits
+        try {
+            WorkBundle wb = new WorkBundle();
+            wul.forEach(wb::addWorkUnit);
+            fail("Did not catch expected exception when adding work unit.");
+        } catch (IllegalStateException e) {
+            // this is the expected outcome.
+        } catch (Throwable t) {
+            fail("Unexpected Exception caught when adding work unit: " + t);
+        }
+    }
+
+    @Test
+    public void testLimitSerDe() throws IOException {
+        // Craft an illegal stream.
+        ByteArrayOutputStream bout = new ByteArrayOutputStream();
+        DataOutputStream out = new DataOutputStream(bout);
+        WorkBundle wb1 = new WorkBundle();
+        // generate test data.
+        final List<WorkUnit> wul = new ArrayList<>();
+        for (int i = 0; i < WorkBundle.MAX_UNITS + 2; i++) {
+            String fileName = UUID.randomUUID().toString();
+            wul.add(new WorkUnit(fileName));
+        }
+
+        WorkBundle.writeUTFOrNull(wb1.bundleId, out);
+        WorkBundle.writeUTFOrNull(wb1.outputRoot, out);
+        WorkBundle.writeUTFOrNull(wb1.eatPrefix, out);
+        WorkBundle.writeUTFOrNull(wb1.caseId, out);
+        WorkBundle.writeUTFOrNull(wb1.sentTo, out);
+        out.writeInt(wb1.errorCount);
+        out.writeInt(wb1.priority);
+        out.writeBoolean(wb1.simpleMode);
+        out.writeLong(wb1.oldestFileModificationTime);
+        out.writeLong(wb1.youngestFileModificationTime);
+        out.writeLong(wb1.totalFileSize);
+        out.writeInt(WorkBundle.MAX_UNITS + 2);
+        for (WorkUnit u : wul) {
+            u.writeToStream(out);
+        }
+
+        out.close();
+        bout.close();
+
+        try {
+            WorkBundle wb2 = WorkBundle.readFromStream(new DataInputStream(new ByteArrayInputStream(bout.toByteArray())));
+            fail("Did not catch expected exception when deserializing work unit.");
+        } catch (IOException e) {
+            // expected
+        } catch (Throwable t) {
+            fail("Unexpected Exception caught when deserializing work unit: " + t);
+
+        }
+    }
 }

programming_language: Java
filename: PayloadUtilTest.java
change_type: ModificationType.MODIFY
diff: @@ -121,14 +121,6 @@ public class PayloadUtilTest extends UnitTest {
         assertEquals("Name of unexpected argument is class name", s.getClass().getName(), PayloadUtil.getName(s));
     }
 
-    @Test
-    public void testByteSerialization() throws Exception {
-        final IBaseDataObject d = DataObjectFactory.getInstance("abc".getBytes(), "testfile", Form.UNKNOWN);
-        final byte[] bytes = PayloadUtil.serializeToBytes(d);
-        final String s1 = new String(bytes);
-        assertTrue("Serializedion must include data from payload", s1.indexOf("abc") > -1);
-    }
-
     @Test
     public void testXmlSerizliaztion() throws Exception {
         final IBaseDataObject d = DataObjectFactory.getInstance("abc".getBytes(), "testfile", Form.UNKNOWN);

programming_language: Java