CVE-2022-39311: Deserialization of Untrusted Data (CWE-502)
value: GoCD is a continuous delivery server. GoCD helps you automate and streamline the build-test-release cycle for continuous delivery of your product. GoCD versions prior to 21.1.0 are vulnerable to remote code execution on the server from a malicious or compromised agent. The Spring RemoteInvocation endpoint exposed agent communication and allowed deserialization of arbitrary java objects, as well as subsequent remote code execution. Exploitation requires agent-level authentication, thus an attacker would need to either compromise an existing agent, its network communication or register a new agent to practically exploit this vulnerability. This issue is fixed in GoCD version 21.1.0. There are currently no known workarounds.
CWE description: The product deserializes untrusted data without sufficiently verifying that the resulting data will be valid.
CVSS v3 base score: 8.8
Published: 2022-10-14T20:15Z
Repository: https://github.com/gocd/gocd
Language: Java
Files changed: server/src/main/java/com/thoughtworks/go/remote/AgentRemoteInvokerServiceExporter.java, server/src/test-fast/java/com/thoughtworks/go/remote/AgentRemoteInvokerServiceExporterTest.java
Commit date: 2021-01-01 13:16:50 -0800
Commit message: Disable RMI endpoint by default

This endpoint can be enabled by including `gocd.agent.remoting.legacy=true` in the
`-Dgocd.agent.extra.properties` property for the server process.

Diff: diff --git a/server/src/main/java/com/thoughtworks/go/remote/AgentRemoteInvokerServiceExporter.java b/server/src/main/java/com/thoughtworks/go/remote/AgentRemoteInvokerServiceExporter.java
index c38caefab61..8da6a040558 100644
--- a/server/src/main/java/com/thoughtworks/go/remote/AgentRemoteInvokerServiceExporter.java
+++ b/server/src/main/java/com/thoughtworks/go/remote/AgentRemoteInvokerServiceExporter.java
@@ -18,10 +18,11 @@ package com.thoughtworks.go.remote;
 
 import com.thoughtworks.go.domain.JobIdentifier;
 import com.thoughtworks.go.domain.JobResult;
 import com.thoughtworks.go.domain.JobState;
 import com.thoughtworks.go.server.service.AgentRuntimeInfo;
+import com.thoughtworks.go.util.SystemEnvironment;
 import org.slf4j.LoggerFactory;
 import org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter;
 import org.springframework.remoting.support.RemoteInvocation;
 import org.springframework.remoting.support.RemoteInvocationResult;
 import org.springframework.web.util.NestedServletException;
@@ -33,13 +34,13 @@ import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.Arrays;
 import java.util.Objects;
 import java.util.Set;
 
+import static com.thoughtworks.go.util.SystemEnvironment.AGENT_EXTRA_PROPERTIES;
 import static java.lang.String.format;
-import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
-import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
+import static javax.servlet.http.HttpServletResponse.*;
 
 /**
  * Custom invoker service exporter that validates UUID authorization on agent requests. This prevents compromised agents
  * (or any other attack masquerading as an authenticated agent) from acting on behalf of another agent.
  */
@@ -53,12 +54,33 @@ public class AgentRemoteInvokerServiceExporter extends HttpInvokerServiceExporte
             new MethodSignature("reportCompleted", AgentRuntimeInfo.class, JobIdentifier.class, JobResult.class),
             new MethodSignature("isIgnored", AgentRuntimeInfo.class, JobIdentifier.class),
             new MethodSignature("getCookie", AgentRuntimeInfo.class)
     );
 
+    private final SystemEnvironment env;
+
+    public AgentRemoteInvokerServiceExporter() {
+        this(new SystemEnvironment());
+    }
+
+    public AgentRemoteInvokerServiceExporter(SystemEnvironment env) {
+        this.env = env;
+    }
+
     @Override
     public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+        if (rejectRMI()) {
+            // yes, ideally, this should be short-circuited in the agent auth filter, but keeping this logic here has
+            // some advantages:
+            //   - it keeps all deprecated RMI logic in one place so it's easier to remove (just remove this class)
+            //   - it's 100% reliable by virtue of its proximity to the RMI invocation code and can't be thwarted by
+            //     some clever URI encoding to circumvent the uri path test that we would need to write at the filter
+            //     level in order to selectively apply this logic to the RMI endpoint and not the JSON API endpoint
+            reject(response, SC_GONE, "This RMI endpoint is disabled.");
+            return;
+        }
+
         try {
             RemoteInvocation invocation = readRemoteInvocation(request);
 
             if (!authorized(request, response, invocation)) {
                 return;
@@ -69,10 +91,15 @@ public class AgentRemoteInvokerServiceExporter extends HttpInvokerServiceExporte
         } catch (ClassNotFoundException ex) {
             throw new NestedServletException("Class not found during deserialization", ex);
         }
     }
 
+    private boolean rejectRMI() {
+        final String props = env.get(AGENT_EXTRA_PROPERTIES).toLowerCase();
+        return !Arrays.asList(props.split("\\s+")).contains("gocd.agent.remoting.legacy=true");
+    }
+
     /**
      * Verifies that the agent UUID from the deserialized payload matches the UUID permitted by the agent authentication
      * filter.
      *
      * @param request    the {@link HttpServletRequest}
diff --git a/server/src/test-fast/java/com/thoughtworks/go/remote/AgentRemoteInvokerServiceExporterTest.java b/server/src/test-fast/java/com/thoughtworks/go/remote/AgentRemoteInvokerServiceExporterTest.java
index 27dc27c9c32..348fd7a1389 100644
--- a/server/src/test-fast/java/com/thoughtworks/go/remote/AgentRemoteInvokerServiceExporterTest.java
+++ b/server/src/test-fast/java/com/thoughtworks/go/remote/AgentRemoteInvokerServiceExporterTest.java
@@ -21,20 +21,22 @@ import com.thoughtworks.go.domain.JobResult;
 import com.thoughtworks.go.domain.JobState;
 import com.thoughtworks.go.http.mocks.MockHttpServletRequest;
 import com.thoughtworks.go.http.mocks.MockHttpServletResponse;
 import com.thoughtworks.go.server.messaging.BuildRepositoryMessageProducer;
 import com.thoughtworks.go.server.service.AgentRuntimeInfo;
+import com.thoughtworks.go.util.SystemEnvironment;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mock;
 import org.springframework.remoting.support.RemoteInvocation;
 import org.springframework.remoting.support.RemoteInvocationResult;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.lang.reflect.InvocationTargetException;
 
+import static com.thoughtworks.go.util.SystemEnvironment.AGENT_EXTRA_PROPERTIES;
 import static javax.servlet.http.HttpServletResponse.*;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.fail;
 import static org.mockito.Mockito.*;
 import static org.mockito.MockitoAnnotations.openMocks;
@@ -44,151 +46,165 @@ public class AgentRemoteInvokerServiceExporterTest {
 
     private MockHttpServletRequest req;
 
     private MockHttpServletResponse res;
 
+    @Mock
+    private SystemEnvironment env;
+
     @Mock
     private BuildRepositoryMessageProducer target;
 
     @BeforeEach
     void setup() throws Exception {
         openMocks(this).close();
+        when(env.get(AGENT_EXTRA_PROPERTIES)).thenReturn("gocd.agent.remoting.legacy=true");
         req = new MockHttpServletRequest();
         req.addHeader("X-Agent-GUID", AGENT_UUID);
         res = new MockHttpServletResponse();
     }
 
+    @Test
+    void rejectsWhenLegacyDisabled() throws Exception {
+        when(env.get(AGENT_EXTRA_PROPERTIES)).thenReturn("");
+        final AgentRuntimeInfo agent = runtimeInfo(AGENT_UUID);
+        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("ping", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}));
+        invoker.handleRequest(req, res);
+        verify(target, never()).ping(agent);
+        assertEquals(SC_GONE, res.getStatus());
+    }
+
     @Test
     void isIgnored_allowedForSameUUID() throws Exception {
         final AgentRuntimeInfo agent = runtimeInfo(AGENT_UUID);
-        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("isIgnored", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class}, new Object[]{agent, null}), target);
+        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("isIgnored", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class}, new Object[]{agent, null}));
         invoker.handleRequest(req, res);
         verify(target, only()).isIgnored(agent, null);
         assertEquals(SC_OK, res.getStatus());
     }
 
     @Test
     void isIgnored_rejectedForDifferentUUID() throws Exception {
         final AgentRuntimeInfo agent = runtimeInfo("other");
-        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("isIgnored", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class}, new Object[]{agent, null}), target);
+        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("isIgnored", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class}, new Object[]{agent, null}));
         invoker.handleRequest(req, res);
         verify(target, never()).isIgnored(any(AgentRuntimeInfo.class), any(JobIdentifier.class));
         assertEquals(SC_FORBIDDEN, res.getStatus());
     }
 
     @Test
     void ping_allowedForSameUUID() throws Exception {
         final AgentRuntimeInfo agent = runtimeInfo(AGENT_UUID);
-        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("ping", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}), target);
+        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("ping", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}));
         invoker.handleRequest(req, res);
         verify(target, only()).ping(agent);
         assertEquals(SC_OK, res.getStatus());
     }
 
     @Test
     void ping_rejectedForDifferentUUID() throws Exception {
         final AgentRuntimeInfo agent = runtimeInfo("other");
-        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("ping", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}), target);
+        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("ping", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}));
         invoker.handleRequest(req, res);
         verify(target, never()).ping(any(AgentRuntimeInfo.class));
         assertEquals(SC_FORBIDDEN, res.getStatus());
     }
 
     @Test
     void getWork_allowedForSameUUID() throws Exception {
         final AgentRuntimeInfo agent = runtimeInfo(AGENT_UUID);
-        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("getWork", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}), target);
+        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("getWork", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}));
         invoker.handleRequest(req, res);
         verify(target, only()).getWork(agent);
         assertEquals(SC_OK, res.getStatus());
     }
 
     @Test
     void getWork_rejectedForDifferentUUID() throws Exception {
         final AgentRuntimeInfo agent = runtimeInfo("other");
-        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("getWork", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}), target);
+        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("getWork", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}));
         invoker.handleRequest(req, res);
         verify(target, never()).getWork(any(AgentRuntimeInfo.class));
         assertEquals(SC_FORBIDDEN, res.getStatus());
     }
 
     @Test
     void getCookie_allowedForSameUUID() throws Exception {
         final AgentRuntimeInfo agent = runtimeInfo(AGENT_UUID);
-        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("getCookie", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}), target);
+        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("getCookie", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}));
         invoker.handleRequest(req, res);
         verify(target, only()).getCookie(agent);
         assertEquals(SC_OK, res.getStatus());
     }
 
     @Test
     void getCookie_rejectedForDifferentUUID() throws Exception {
         final AgentRuntimeInfo agent = runtimeInfo("other");
-        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("getCookie", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}), target);
+        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("getCookie", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}));
         invoker.handleRequest(req, res);
         verify(target, never()).getCookie(any(AgentRuntimeInfo.class));
         assertEquals(SC_FORBIDDEN, res.getStatus());
     }
 
     @Test
     void reportCurrentStatus_allowedForSameUUID() throws Exception {
         final AgentRuntimeInfo agent = runtimeInfo(AGENT_UUID);
-        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCurrentStatus", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobState.class}, new Object[]{agent, null, null}), target);
+        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCurrentStatus", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobState.class}, new Object[]{agent, null, null}));
         invoker.handleRequest(req, res);
         verify(target, only()).reportCurrentStatus(agent, null, null);
         assertEquals(SC_OK, res.getStatus());
     }
 
     @Test
     void reportCurrentStatus_rejectedForDifferentUUID() throws Exception {
         final AgentRuntimeInfo agent = runtimeInfo("other");
-        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCurrentStatus", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobState.class}, new Object[]{agent, null, null}), target);
+        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCurrentStatus", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobState.class}, new Object[]{agent, null, null}));
         invoker.handleRequest(req, res);
         verify(target, never()).reportCurrentStatus(any(AgentRuntimeInfo.class), any(JobIdentifier.class), any(JobState.class));
         assertEquals(SC_FORBIDDEN, res.getStatus());
     }
 
     @Test
     void reportCompleting_allowedForSameUUID() throws Exception {
         final AgentRuntimeInfo agent = runtimeInfo(AGENT_UUID);
-        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCompleting", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobResult.class}, new Object[]{agent, null, null}), target);
+        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCompleting", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobResult.class}, new Object[]{agent, null, null}));
         invoker.handleRequest(req, res);
         verify(target, only()).reportCompleting(agent, null, null);
         assertEquals(SC_OK, res.getStatus());
     }
 
     @Test
     void reportCompleting_rejectedForDifferentUUID() throws Exception {
         final AgentRuntimeInfo agent = runtimeInfo("other");
-        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCompleting", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobResult.class}, new Object[]{agent, null, null}), target);
+        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCompleting", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobResult.class}, new Object[]{agent, null, null}));
         invoker.handleRequest(req, res);
         verify(target, never()).reportCompleting(any(AgentRuntimeInfo.class), any(JobIdentifier.class), any(JobResult.class));
         assertEquals(SC_FORBIDDEN, res.getStatus());
     }
 
     @Test
     void reportCompleted_allowedForSameUUID() throws Exception {
         final AgentRuntimeInfo agent = runtimeInfo(AGENT_UUID);
-        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCompleted", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobResult.class}, new Object[]{agent, null, null}), target);
+        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCompleted", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobResult.class}, new Object[]{agent, null, null}));
         invoker.handleRequest(req, res);
         verify(target, only()).reportCompleted(agent, null, null);
         assertEquals(SC_OK, res.getStatus());
     }
 
     @Test
     void reportCompleted_rejectedForDifferentUUID() throws Exception {
         final AgentRuntimeInfo agent = runtimeInfo("other");
-        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCompleted", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobResult.class}, new Object[]{agent, null, null}), target);
+        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCompleted", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobResult.class}, new Object[]{agent, null, null}));
         invoker.handleRequest(req, res);
         verify(target, never()).reportCompleted(any(AgentRuntimeInfo.class), any(JobIdentifier.class), any(JobResult.class));
         assertEquals(SC_FORBIDDEN, res.getStatus());
     }
 
     @Test
     void rejectsUnknownMethod() throws Exception {
         final AgentRuntimeInfo agent = runtimeInfo(AGENT_UUID);
-        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("nonexistent", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}), target);
+        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("nonexistent", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}));
         invoker.handleRequest(req, res);
         verifyNoInteractions(target);
         assertEquals(SC_BAD_REQUEST, res.getStatus());
     }
 
@@ -198,15 +214,15 @@ public class AgentRemoteInvokerServiceExporterTest {
 
     private AgentIdentifier identifier(String uuid) {
         return new AgentIdentifier(null, null, uuid);
     }
 
-    private static AgentRemoteInvokerServiceExporter deserializingWith(final RemoteInvocation invocation, final BuildRepositoryMessageProducer proxy) {
-        final AgentRemoteInvokerServiceExporter invoker = new AgentRemoteInvokerServiceExporter() {
+    private AgentRemoteInvokerServiceExporter deserializingWith(final RemoteInvocation invocation) {
+        final AgentRemoteInvokerServiceExporter invoker = new AgentRemoteInvokerServiceExporter(env) {
             @Override
             protected Object getProxyForService() {
-                return proxy;
+                return target;
             }
 
             @Override
             protected RemoteInvocation readRemoteInvocation(HttpServletRequest request) {
                 return invocation;

Vulnerable code: import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("isIgnored", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class}, new Object[]{agent, null}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("isIgnored", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class}, new Object[]{agent, null}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("ping", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("ping", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("getWork", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("getWork", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("getCookie", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("getCookie", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCurrentStatus", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobState.class}, new Object[]{agent, null, null}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCurrentStatus", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobState.class}, new Object[]{agent, null, null}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCompleting", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobResult.class}, new Object[]{agent, null, null}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCompleting", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobResult.class}, new Object[]{agent, null, null}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCompleted", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobResult.class}, new Object[]{agent, null, null}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCompleted", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobResult.class}, new Object[]{agent, null, null}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("nonexistent", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}), target);
    private static AgentRemoteInvokerServiceExporter deserializingWith(final RemoteInvocation invocation, final BuildRepositoryMessageProducer proxy) {
        final AgentRemoteInvokerServiceExporter invoker = new AgentRemoteInvokerServiceExporter() {
                return proxy;
Fixed code: import com.thoughtworks.go.util.SystemEnvironment;
import static com.thoughtworks.go.util.SystemEnvironment.AGENT_EXTRA_PROPERTIES;
import static javax.servlet.http.HttpServletResponse.*;
    private final SystemEnvironment env;

    public AgentRemoteInvokerServiceExporter() {
        this(new SystemEnvironment());
    }

    public AgentRemoteInvokerServiceExporter(SystemEnvironment env) {
        this.env = env;
    }

        if (rejectRMI()) {
            // yes, ideally, this should be short-circuited in the agent auth filter, but keeping this logic here has
            // some advantages:
            //   - it keeps all deprecated RMI logic in one place so it's easier to remove (just remove this class)
            //   - it's 100% reliable by virtue of its proximity to the RMI invocation code and can't be thwarted by
            //     some clever URI encoding to circumvent the uri path test that we would need to write at the filter
            //     level in order to selectively apply this logic to the RMI endpoint and not the JSON API endpoint
            reject(response, SC_GONE, "This RMI endpoint is disabled.");
            return;
        }

    private boolean rejectRMI() {
        final String props = env.get(AGENT_EXTRA_PROPERTIES).toLowerCase();
        return !Arrays.asList(props.split("\\s+")).contains("gocd.agent.remoting.legacy=true");
    }

import com.thoughtworks.go.util.SystemEnvironment;
import static com.thoughtworks.go.util.SystemEnvironment.AGENT_EXTRA_PROPERTIES;
    @Mock
    private SystemEnvironment env;

        when(env.get(AGENT_EXTRA_PROPERTIES)).thenReturn("gocd.agent.remoting.legacy=true");
    @Test
    void rejectsWhenLegacyDisabled() throws Exception {
        when(env.get(AGENT_EXTRA_PROPERTIES)).thenReturn("");
        final AgentRuntimeInfo agent = runtimeInfo(AGENT_UUID);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("ping", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}));
        invoker.handleRequest(req, res);
        verify(target, never()).ping(agent);
        assertEquals(SC_GONE, res.getStatus());
    }

        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("isIgnored", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class}, new Object[]{agent, null}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("isIgnored", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class}, new Object[]{agent, null}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("ping", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("ping", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("getWork", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("getWork", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("getCookie", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("getCookie", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCurrentStatus", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobState.class}, new Object[]{agent, null, null}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCurrentStatus", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobState.class}, new Object[]{agent, null, null}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCompleting", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobResult.class}, new Object[]{agent, null, null}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCompleting", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobResult.class}, new Object[]{agent, null, null}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCompleted", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobResult.class}, new Object[]{agent, null, null}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCompleted", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobResult.class}, new Object[]{agent, null, null}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("nonexistent", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}));
    private AgentRemoteInvokerServiceExporter deserializingWith(final RemoteInvocation invocation) {
        final AgentRemoteInvokerServiceExporter invoker = new AgentRemoteInvokerServiceExporter(env) {
                return target;