CVE-2021-32634: Deserialization of Untrusted Data (CWE-502)
value: 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.
CWE description: The product deserializes untrusted data without sufficiently verifying that the resulting data will be valid.
CVSS v3 base score: 7.2
Published: 2021-05-21T18:15Z
Repository: https://github.com/NationalSecurityAgency/emissary
Language: Java
Files changed: src/main/java/emissary/core/IMobileAgent.java, src/main/java/emissary/pickup/WorkBundle.java, src/main/java/emissary/pickup/WorkUnit.java, src/main/java/emissary/server/mvc/adapters/WorkSpaceAdapter.java, src/main/java/emissary/server/mvc/internal/WorkSpaceClientEnqueueAction.java, src/main/java/emissary/util/PayloadUtil.java, src/test/java/emissary/pickup/WorkBundleTest.java, src/test/java/emissary/util/PayloadUtilTest.java
Commit date: 2021-05-21 10:32:31 -0400
Commit message: 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
Diff: diff --git a/src/main/java/emissary/core/IMobileAgent.java b/src/main/java/emissary/core/IMobileAgent.java
index ad81768..99164ff 100755
--- a/src/main/java/emissary/core/IMobileAgent.java
+++ b/src/main/java/emissary/core/IMobileAgent.java
@@ -1,12 +1,10 @@
 package emissary.core;
 
 import java.io.Serializable;
 import java.util.List;
 
-import emissary.server.mvc.adapters.MoveToAdapter;
-
 /**
  * Interface to the MobileAgent
  */
 public interface IMobileAgent extends Serializable, Runnable {
     /**
@@ -57,13 +55,12 @@ public interface IMobileAgent extends Serializable, Runnable {
      * Return true if agent is working on a payload
      */
     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();
 
     /**
      * Get the name of the current payload
diff --git a/src/main/java/emissary/pickup/WorkBundle.java b/src/main/java/emissary/pickup/WorkBundle.java
index 1607f04..4ac5cfe 100644
--- a/src/main/java/emissary/pickup/WorkBundle.java
+++ b/src/main/java/emissary/pickup/WorkBundle.java
@@ -1,8 +1,10 @@
 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;
 import java.util.UUID;
 
@@ -19,17 +21,16 @@ import org.slf4j.LoggerFactory;
  * modification time, both initially entered as time from the epoch. However, the concept of "youngest" and "oldest" is
  * relative to the construction time, so that:
  * <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;
 
     // Configured output root for finding data on remote side
     String outputRoot;
@@ -108,10 +109,84 @@ public class WorkBundle implements Serializable, Comparable<WorkBundle> {
             this.addWorkUnits(that.getWorkUnitList());
         }
         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
      * 
      * @param val the new value to set as bundle id
      */
@@ -202,25 +277,34 @@ public class WorkBundle implements Serializable, Comparable<WorkBundle> {
     /**
      * Add a workUnit to the list.
      *
      * @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();
     }
 
     /**
      * Add a workunit to the list
      * 
      * @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;
         }
         if (fileModificationTimeInMillis > youngestFileModificationTime) {
             youngestFileModificationTime = fileModificationTimeInMillis;
@@ -229,12 +313,20 @@ public class WorkBundle implements Serializable, Comparable<WorkBundle> {
         return size();
     }
 
     /**
      * 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();
     }
 
     /**
@@ -263,47 +355,57 @@ public class WorkBundle implements Serializable, Comparable<WorkBundle> {
     /**
      * Add a file to the list, without adjusting file modification time tracking.
      * 
      * @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));
     }
 
     /**
      * Add a file to the list
      * 
      * @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);
     }
 
     /**
      * Add files to the list, without adjusting file modification time tracking.
      * 
      * @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();
     }
 
     /**
diff --git a/src/main/java/emissary/pickup/WorkUnit.java b/src/main/java/emissary/pickup/WorkUnit.java
index 7cfae21..92a08ba 100644
--- a/src/main/java/emissary/pickup/WorkUnit.java
+++ b/src/main/java/emissary/pickup/WorkUnit.java
@@ -1,12 +1,16 @@
 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
     private boolean failedToParse = false;
     private boolean failedToProcess = false;
@@ -34,10 +38,26 @@ public class WorkUnit {
         this.transactionId = transactionId;
         this.failedToParse = failedToParse;
         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
      * 
      * @return the filename
      */
diff --git a/src/main/java/emissary/server/mvc/adapters/MoveToAdapter.java b/src/main/java/emissary/server/mvc/adapters/MoveToAdapter.java
deleted file mode 100755
index 63ebd7d..0000000
--- a/src/main/java/emissary/server/mvc/adapters/MoveToAdapter.java
+++ /dev/null
@@ -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;
-        }
-    }
-}
diff --git a/src/main/java/emissary/server/mvc/adapters/WorkSpaceAdapter.java b/src/main/java/emissary/server/mvc/adapters/WorkSpaceAdapter.java
index dc46a65..e3ce7a4 100755
--- a/src/main/java/emissary/server/mvc/adapters/WorkSpaceAdapter.java
+++ b/src/main/java/emissary/server/mvc/adapters/WorkSpaceAdapter.java
@@ -1,10 +1,10 @@
 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;
 import java.util.List;
 
@@ -299,12 +299,12 @@ public class WorkSpaceAdapter extends EmissaryClient {
         /**
          * Sets the WorkBundle object from serialized data
          */
         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");
             }
         }
diff --git a/src/main/java/emissary/server/mvc/internal/MoveToAction.java b/src/main/java/emissary/server/mvc/internal/MoveToAction.java
deleted file mode 100644
index 6966f48..0000000
--- a/src/main/java/emissary/server/mvc/internal/MoveToAction.java
+++ /dev/null
@@ -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();
-    }
-
-}
diff --git a/src/main/java/emissary/server/mvc/internal/WorkSpaceClientEnqueueAction.java b/src/main/java/emissary/server/mvc/internal/WorkSpaceClientEnqueueAction.java
index 0fd3371..d987097 100644
--- a/src/main/java/emissary/server/mvc/internal/WorkSpaceClientEnqueueAction.java
+++ b/src/main/java/emissary/server/mvc/internal/WorkSpaceClientEnqueueAction.java
@@ -1,9 +1,9 @@
 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;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
@@ -47,12 +47,12 @@ public class WorkSpaceClientEnqueueAction {
             final IPickUpSpace place = (IPickUpSpace) Namespace.lookup(nsName);
             if (place == null) {
                 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);
             return Response.serverError().entity("WorkSpaceClientEnqueWorker exception:\n" + e.getMessage()).build();
         }
diff --git a/src/main/java/emissary/util/PayloadUtil.java b/src/main/java/emissary/util/PayloadUtil.java
index 8c9b307..f37cc2e 100755
--- a/src/main/java/emissary/util/PayloadUtil.java
+++ b/src/main/java/emissary/util/PayloadUtil.java
@@ -1,15 +1,7 @@
 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;
 import java.util.List;
 import java.util.Map;
@@ -107,77 +99,10 @@ public class PayloadUtil {
         sb.append("//").append(payload.getFileType());
         sb.append("//").append(payload.getCreationTimestamp());
         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
      * 
      * @param d the payload
      */
@@ -201,11 +126,11 @@ public class PayloadUtil {
             root.addContent(JDOMUtil.simpleElement("processing-error", d.getProcessingError()));
         }
         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);
 
         if (d.header() != null) {
@@ -228,12 +153,11 @@ public class PayloadUtil {
             }
             root.addContent(views);
         }
 
         logger.debug("Produced xml document for " + d.shortName());
-        final Document doc = new Document(root);
-        return doc;
+        return new Document(root);
     }
 
     /**
      * Turn the payload into an xml string
      * 
@@ -253,12 +177,11 @@ public class PayloadUtil {
         for (final IBaseDataObject d : list) {
             final Document doc = toXml(d);
             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);
     }
 
     /**
      * Turn the payload list into an xml string
      * 
@@ -275,11 +198,11 @@ public class PayloadUtil {
 
     public static String printFormattedMetadata(final IBaseDataObject payload) {
         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();
     }
 
     /** This class is not meant to be instantiated. */
diff --git a/src/test/java/emissary/pickup/WorkBundleTest.java b/src/test/java/emissary/pickup/WorkBundleTest.java
index f354f49..1c825a0 100644
--- a/src/test/java/emissary/pickup/WorkBundleTest.java
+++ b/src/test/java/emissary/pickup/WorkBundleTest.java
@@ -5,10 +5,16 @@ import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 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;
 import java.util.UUID;
 
@@ -231,6 +237,102 @@ public class WorkBundleTest extends UnitTest {
         w.addFileName("<file2.txt&foo=bar>", 7L, 10L);
         assertEquals(15L, w.getYoungestFileModificationTime());
         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);
+
+        }
+    }
 }
diff --git a/src/test/java/emissary/util/PayloadUtilTest.java b/src/test/java/emissary/util/PayloadUtilTest.java
index 611dcb9..379d07e 100644
--- a/src/test/java/emissary/util/PayloadUtilTest.java
+++ b/src/test/java/emissary/util/PayloadUtilTest.java
@@ -119,18 +119,10 @@ public class PayloadUtilTest extends UnitTest {
     public void testNameOfBadArgument() {
         final String s = "foo";
         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);
         d.addAlternateView("AV", "def".getBytes());
         d.putParameter("P", "ghi");

Vulnerable code: import emissary.server.mvc.adapters.MoveToAdapter;

     * Get the payload as an object for serialization during transport Should only be called by the MoveToAdapter
     * 
     * @see MoveToAdapter
import java.io.Serializable;
public class WorkBundle implements Serializable, Comparable<WorkBundle> {

    // Serializable
    static final long serialVersionUID = 6339812801001572532L;
        workUnitList.add(workUnit);
        workUnitList.add(new WorkUnit(file));
        return size();
        for (int i = 0; file != null && i < file.length; i++) {
            workUnitList.add(new WorkUnit(file[i]));
            workUnitList.add(new WorkUnit(file));
public class WorkUnit {
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;
        }
    }
}
import java.io.ObjectInputStream;
                final ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(s.getBytes("8859_1")));
                this.paths = (WorkBundle) ois.readObject();
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();
    }

}
import java.io.ObjectInputStream;
            final ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(workBundleString.getBytes("8859_1")));
            WorkBundle paths = (WorkBundle) ois.readObject();
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;
    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
                }
            }
        }
    }

            m.setAttribute("name", key.toString());
        final Document doc = new Document(root);
        return doc;
        final Document doc = new Document(root);
        return doc;
            out.append(entry.getKey() + SEP + entry.getValue() + LS);
    @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);
    }

Fixed code:      * Get the payload as an object for serialization during transport.
     *
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
public final class WorkBundle implements Comparable<WorkBundle> {
    static final int MAX_UNITS = 1024;

    /**
     * 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);
        }
    }

     * @throws IllegalStateException if adding the unit would cause the bundle to contain more than <code>MAX_UNITS</code>
     *         work units
        if (workUnitList.size() >= MAX_UNITS) {
            throw new IllegalStateException("WorkBundle may not contain more than " + MAX_UNITS + " WorkUnits.");
        }
     * @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
        addWorkUnit(workUnit);

     * 
     * @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
        if (workUnitList.size() + list.size() > MAX_UNITS) {
            throw new IllegalStateException("WorkBundle may not contain more than " + MAX_UNITS + " WorkUnits.");
        }
     * @throws IllegalStateException if adding the file would cause the bundle to contain more than <code>MAX_UNITS</code>
     *         work units
        return addWorkUnit(new WorkUnit(file));
     * @param fileSize the size of the file being added
     * @throws IllegalStateException if adding the file would cause the bundle to contain more than <code>MAX_UNITS</code>
     *         work units
     * @throws IllegalStateException if adding the files would cause the bundle to contain more than <code>MAX_UNITS</code>
     *         work units
        for (String f : file) {
            addWorkUnit(new WorkUnit(f));
     * 
     * @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
            addWorkUnit(new WorkUnit(file));
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;

public final class WorkUnit {
    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);
    }

import java.io.DataInputStream;
                final DataInputStream dis = new DataInputStream(new ByteArrayInputStream(s.getBytes("8859_1")));
                this.paths = WorkBundle.readFromStream(dis);
import java.io.DataInputStream;
            final DataInputStream ois = new DataInputStream(new ByteArrayInputStream(workBundleString.getBytes("8859_1")));
            WorkBundle paths = WorkBundle.readFromStream(ois);
            m.setAttribute("name", key);
        return new Document(root);
        return new Document(root);
            out.append(entry.getKey()).append(SEP).append(entry.getValue()).append(LS);
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
    @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);

        }
    }