CVE-2018-1000129: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: An XSS vulnerability exists in the Jolokia agent version 1.3.7 in the HTTP servlet that allows an attacker to execute malicious javascript in the victim's browser.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 6.1
Published: 2018-03-14T13:29Z
Repository: https://github.com/rhuss/jolokia
Language: Java
Files changed: agent/core/src/main/java/org/jolokia/http/AgentServlet.java, agent/core/src/main/java/org/jolokia/util/MimeTypeUtil.java, agent/core/src/test/java/org/jolokia/http/AgentServletTest.java, agent/jvm/src/main/java/org/jolokia/jvmagent/handler/JolokiaHttpHandler.java, agent/jvm/src/test/java/org/jolokia/jvmagent/handler/JolokiaHttpHandlerTest.java
Commit date: 2018-01-24 10:35:53 +0100
Commit message: fix: Verify a given 'mimeType' and/or 'callback' request parameter

So that only fixed values are possible, in order to avoid XSS attack
vectors.

Diff: diff --git a/agent/core/src/main/java/org/jolokia/http/AgentServlet.java b/agent/core/src/main/java/org/jolokia/http/AgentServlet.java
index e9adc3a9..6a0e73b1 100755
--- a/agent/core/src/main/java/org/jolokia/http/AgentServlet.java
+++ b/agent/core/src/main/java/org/jolokia/http/AgentServlet.java
@@ -262,24 +262,32 @@ public class AgentServlet extends HttpServlet {
     }
 
     @SuppressWarnings({ "PMD.AvoidCatchingThrowable", "PMD.AvoidInstanceofChecksInCatchClause" })
     private void handle(ServletRequestHandler pReqHandler,HttpServletRequest pReq, HttpServletResponse pResp) throws IOException {
         JSONAware json = null;
+
         try {
             // Check access policy
             requestHandler.checkAccess(allowDnsReverseLookup ? pReq.getRemoteHost() : null,
                                        pReq.getRemoteAddr(),
                                        getOriginOrReferer(pReq));
 
+            // If a callback is given, check this is a valid javascript function name
+            validateCallbackIfGiven(pReq);
+
             // Remember the agent URL upon the first request. Needed for discovery
             updateAgentDetailsIfNeeded(pReq);
 
             // Dispatch for the proper HTTP request method
             json = handleSecurely(pReqHandler, pReq, pResp);
         } catch (Throwable exp) {
-            json = requestHandler.handleThrowable(
+            try {
+                json = requestHandler.handleThrowable(
                     exp instanceof RuntimeMBeanException ? ((RuntimeMBeanException) exp).getTargetException() : exp);
+            } catch (Throwable exp2) {
+                exp2.printStackTrace();
+            }
         } finally {
             setCorsHeader(pReq, pResp);
 
             if (json == null) {
                 json = requestHandler.handleThrowable(new Exception("Internal error while handling an exception"));
@@ -287,10 +295,11 @@ public class AgentServlet extends HttpServlet {
 
             sendResponse(pResp, pReq, json);
         }
     }
 
+
     private JSONAware handleSecurely(final ServletRequestHandler pReqHandler, final HttpServletRequest pReq, final HttpServletResponse pResp) throws IOException, PrivilegedActionException {
         Subject subject = (Subject) pReq.getAttribute(ConfigKey.JAAS_SUBJECT_REQUEST_ATTRIBUTE);
         if (subject != null) {
             return Subject.doAs(subject, new PrivilegedExceptionAction<JSONAware>() {
                     public JSONAware run() throws IOException {
@@ -378,19 +387,10 @@ public class AgentServlet extends HttpServlet {
             pResp.setHeader("Access-Control-Allow-Origin", origin);
             pResp.setHeader("Access-Control-Allow-Credentials","true");
         }
     }
 
-    // Extract mime type for response (if not JSONP)
-    private String getMimeType(HttpServletRequest pReq) {
-        String requestMimeType = pReq.getParameter(ConfigKey.MIME_TYPE.getKeyValue());
-        if (requestMimeType != null) {
-            return requestMimeType;
-        }
-        return configMimeType;
-    }
-
     private boolean isStreamingEnabled(HttpServletRequest pReq) {
         String streamingFromReq = pReq.getParameter(ConfigKey.STREAMING.getKeyValue());
         if (streamingFromReq != null) {
             return Boolean.parseBoolean(streamingFromReq);
         }
@@ -468,12 +468,16 @@ public class AgentServlet extends HttpServlet {
         return config;
     }
 
     private void sendResponse(HttpServletResponse pResp, HttpServletRequest pReq, JSONAware pJson) throws IOException {
         String callback = pReq.getParameter(ConfigKey.CALLBACK.getKeyValue());
-        setContentType(pResp, callback != null ? "text/javascript" : getMimeType(pReq));
 
+        setContentType(pResp,
+                       MimeTypeUtil.getResponseMimeType(
+                           pReq.getParameter(ConfigKey.MIME_TYPE.getKeyValue()),
+                           configMimeType, callback
+                                                       ));
         pResp.setStatus(HttpServletResponse.SC_OK);
         setNoCacheHeaders(pResp);
         if (pJson == null) {
             pResp.setContentLength(-1);
         } else {
@@ -485,10 +489,16 @@ public class AgentServlet extends HttpServlet {
                 sendAllJSON(pResp, callback, pJson);
             }
         }
     }
 
+    private void validateCallbackIfGiven(HttpServletRequest pReq) {
+        String callback = pReq.getParameter(ConfigKey.CALLBACK.getKeyValue());
+        if (callback != null && !MimeTypeUtil.isValidCallback(callback)) {
+            throw new IllegalArgumentException("Invalid callback name given, which must be a valid javascript function name");
+        }
+    }
     private void sendStreamingResponse(HttpServletResponse pResp, String pCallback, JSONStreamAware pJson) throws IOException {
         Writer writer = new OutputStreamWriter(pResp.getOutputStream(), "UTF-8");
         IoUtil.streamResponseAndClose(writer, pJson, pCallback);
     }
 
diff --git a/agent/core/src/main/java/org/jolokia/util/MimeTypeUtil.java b/agent/core/src/main/java/org/jolokia/util/MimeTypeUtil.java
new file mode 100644
index 00000000..e6078f22
--- /dev/null
+++ b/agent/core/src/main/java/org/jolokia/util/MimeTypeUtil.java
@@ -0,0 +1,74 @@
+package org.jolokia.util;
+
+import java.util.regex.Pattern;
+
+/**
+ * Helper class for handling proper response mime types
+ *
+ * @author roland
+ * @since 24.01.18
+ */
+public class MimeTypeUtil {
+
+
+    /**
+     * Extract the response mime type. This value is calculated for different situations:
+     * <p>
+     * <ul>
+     * <li>If a callback is given and its valid, the mime type is "text/javascript"</li>
+     * <li>Otherwise:
+     * <ul>
+     * <li>If a valid mimeType is given in the request ("text/plain", "application/json"), then this
+     * mimet type is returned</li>
+     * <li>If another mimeType is given, then "text/plain" is used</li>
+     * <li>If no mimeType is given then a given default mime type is used, but also sanitized
+     * as described above</li>
+     * </ul>
+     * </li>
+     * </ul>
+     *
+     * @param pRequestMimeType the mimetype given in the request
+     * @param defaultMimeType  the default mime type to use if none is given in the request
+     * @param pCallback        a callback given (can be null)
+     */
+    public static String getResponseMimeType(String pRequestMimeType, String defaultMimeType, String pCallback) {
+
+        // For a valid given callback, return "text/javascript" for proper inclusion
+        if (pCallback != null && isValidCallback(pCallback)) {
+            return "text/javascript";
+        }
+
+        // Pick up mime time from request, but sanitize
+        if (pRequestMimeType != null) {
+            return sanitize(pRequestMimeType);
+        }
+
+        // Use the given default mime type (possibly picked up from a configuration)
+        return sanitize(defaultMimeType);
+    }
+
+    private static String sanitize(String mimeType) {
+        for (String accepted : new String[]{
+            "application/json",
+            "text/plain"
+        }) {
+            if (accepted.equalsIgnoreCase(mimeType)) {
+                return accepted;
+            }
+        }
+        return "text/plain";
+    }
+
+    /**
+     * Check that a callback matches a javascript function name. The argument must be not null
+     *
+     * @param pCallback callback to verify
+     * @return true if valud, false otherwise
+     */
+    public static boolean isValidCallback(String pCallback) {
+        Pattern validJavaScriptFunctionNamePattern =
+            Pattern.compile("^[$A-Z_][0-9A-Z_$]*$", Pattern.CASE_INSENSITIVE);
+        return validJavaScriptFunctionNamePattern.matcher(pCallback).matches();
+    }
+
+}
\ No newline at end of file
diff --git a/agent/core/src/test/java/org/jolokia/http/AgentServletTest.java b/agent/core/src/test/java/org/jolokia/http/AgentServletTest.java
index 52e7f3d2..a494725d 100755
--- a/agent/core/src/test/java/org/jolokia/http/AgentServletTest.java
+++ b/agent/core/src/test/java/org/jolokia/http/AgentServletTest.java
@@ -111,11 +111,11 @@ public class AgentServletTest {
         assertNull(cfg.get(ConfigKey.DETECTOR_OPTIONS));
 
     }
 
     @Test
-    public void initWithcustomAccessRestrictor() throws ServletException {
+    public void initWithCustomAccessRestrictor() throws ServletException {
         prepareStandardInitialisation();
         servlet.destroy();
     }
 
     @Test
@@ -249,10 +249,48 @@ public class AgentServletTest {
 
         assertTrue(sw.toString().contains("used"));
         servlet.destroy();
     }
 
+    @Test
+    public void simpleGetWithWrongMimeType() throws ServletException, IOException {
+        checkMimeTypes("text/html", "text/plain");
+    }
+
+    @Test
+    public void simpleGetWithTextPlainMimeType() throws ServletException, IOException {
+        checkMimeTypes("text/plain", "text/plain");
+    }
+
+    @Test
+    public void simpleGetWithApplicationJsonMimeType() throws ServletException, IOException {
+        checkMimeTypes("application/json", "application/json");
+    }
+
+    private void checkMimeTypes(String given, final String expected) throws ServletException, IOException {
+        prepareStandardInitialisation();
+
+        initRequestResponseMocks(
+            getStandardRequestSetup(),
+            new Runnable() {
+                public void run() {
+                    response.setCharacterEncoding("utf-8");
+                    // The default content type
+                    response.setContentType(expected);
+                    response.setStatus(200);
+                }
+            });
+        expect(request.getPathInfo()).andReturn(HttpTestUtil.HEAP_MEMORY_GET_REQUEST);
+        expect(request.getParameter(ConfigKey.MIME_TYPE.getKeyValue())).andReturn(given);
+        replay(request, response);
+
+        servlet.doGet(request, response);
+
+        verifyMocks();
+        servlet.destroy();
+    }
+
     @Test
     public void simpleGetWithNoReverseDnsLookupFalse() throws ServletException, IOException {
         checkNoReverseDns(false,"127.0.0.1");
     }
 
@@ -482,19 +520,44 @@ public class AgentServletTest {
                         response.setStatus(200);
                     }
                 });
         expect(request.getPathInfo()).andReturn(HttpTestUtil.HEAP_MEMORY_GET_REQUEST);
         expect(request.getAttribute("subject")).andReturn(null);
+        expect(request.getParameter(ConfigKey.MIME_TYPE.getKeyValue())).andReturn(null);
 
         replay(request, response);
 
         servlet.doGet(request, response);
 
         assertTrue(sw.toString().matches("^myCallback\\(.*\\);$"));
         servlet.destroy();
     }
 
+    @Test
+    public void withInvalidCallback() throws IOException, ServletException {
+        servlet = new AgentServlet(new AllowAllRestrictor());
+        initConfigMocks(null, null,"Error 400", IllegalArgumentException.class);
+        replay(config, context);
+        servlet.init(config);
+        ByteArrayOutputStream sw = initRequestResponseMocks(
+            "doSomethingEvil(); myCallback",
+            getStandardRequestSetup(),
+            getStandardResponseSetup());
+        expect(request.getPathInfo()).andReturn(HttpTestUtil.HEAP_MEMORY_GET_REQUEST);
+        expect(request.getAttribute("subject")).andReturn(null);
+        expect(request.getParameter(ConfigKey.MIME_TYPE.getKeyValue())).andReturn(null);
+
+        replay(request, response);
+
+        servlet.doGet(request, response);
+        String resp = sw.toString();
+        assertTrue(resp.contains("error_type"));
+        assertTrue(resp.contains("IllegalArgumentException"));
+        assertTrue(resp.matches(".*status.*400.*"));
+        servlet.destroy();
+    }
+
     @Test
     public void withException() throws ServletException, IOException {
         servlet = new AgentServlet(new AllowAllRestrictor());
         initConfigMocks(null, null,"Error 500", IllegalStateException.class);
         replay(config, context);
@@ -604,11 +667,11 @@ public class AgentServletTest {
     private ByteArrayOutputStream initRequestResponseMocks(String callback,Runnable requestSetup,Runnable responseSetup) throws IOException {
         request = createMock(HttpServletRequest.class);
         response = createMock(HttpServletResponse.class);
         setNoCacheHeaders(response);
 
-        expect(request.getParameter(ConfigKey.CALLBACK.getKeyValue())).andReturn(callback);
+        expect(request.getParameter(ConfigKey.CALLBACK.getKeyValue())).andReturn(callback).anyTimes();
         requestSetup.run();
         responseSetup.run();
 
         class MyServletOutputStream extends ServletOutputStream {
             ByteArrayOutputStream baos;
@@ -646,10 +709,11 @@ public class AgentServletTest {
 
     private Runnable getStandardResponseSetup() {
         return new Runnable() {
             public void run() {
                 response.setCharacterEncoding("utf-8");
+                // The default content type
                 response.setContentType("text/plain");
                 response.setStatus(200);
             }
         };
     }
@@ -688,11 +752,11 @@ public class AgentServletTest {
                 expect(request.getRequestURI()).andReturn("/jolokia/");
                 expect(request.getParameterMap()).andReturn(null);
                 expect(request.getAttribute(ConfigKey.JAAS_SUBJECT_REQUEST_ATTRIBUTE)).andReturn(null);
 
                 expect(request.getPathInfo()).andReturn(HttpTestUtil.HEAP_MEMORY_GET_REQUEST);
-                expect(request.getParameter(ConfigKey.MIME_TYPE.getKeyValue())).andReturn("text/plain");
+                expect(request.getParameter(ConfigKey.MIME_TYPE.getKeyValue())).andReturn("text/plain").anyTimes();
                 StringBuffer buf = new StringBuffer();
                 buf.append(url).append(HttpTestUtil.HEAP_MEMORY_GET_REQUEST);
                 expect(request.getRequestURL()).andReturn(buf);
                 expect(request.getRequestURI()).andReturn("/jolokia" + HttpTestUtil.HEAP_MEMORY_GET_REQUEST);
                 expect(request.getContextPath()).andReturn("/jolokia");
diff --git a/agent/jvm/src/main/java/org/jolokia/jvmagent/handler/JolokiaHttpHandler.java b/agent/jvm/src/main/java/org/jolokia/jvmagent/handler/JolokiaHttpHandler.java
index 9dc0df07..83eb64d9 100644
--- a/agent/jvm/src/main/java/org/jolokia/jvmagent/handler/JolokiaHttpHandler.java
+++ b/agent/jvm/src/main/java/org/jolokia/jvmagent/handler/JolokiaHttpHandler.java
@@ -222,10 +222,13 @@ public class JolokiaHttpHandler implements HttpHandler {
             requestHandler.checkAccess(getHostName(address),
                                        address.getAddress().getHostAddress(),
                                        extractOriginOrReferer(pExchange));
             String method = pExchange.getRequestMethod();
 
+            // If a callback is given, check this is a valid javascript function name
+            validateCallbackIfGiven(parsedUri);
+
             // Dispatch for the proper HTTP request method
             if ("GET".equalsIgnoreCase(method)) {
                 setHeaders(pExchange);
                 json = executeGetRequest(parsedUri);
             } else if ("POST".equalsIgnoreCase(method)) {
@@ -242,10 +245,18 @@ public class JolokiaHttpHandler implements HttpHandler {
         } finally {
             sendResponse(pExchange, parsedUri, json);
         }
     }
 
+
+    private void validateCallbackIfGiven(ParsedUri pUri) {
+        String callback = pUri.getParameter(ConfigKey.CALLBACK.getKeyValue());
+        if (callback != null && !MimeTypeUtil.isValidCallback(callback)) {
+            throw new IllegalArgumentException("Invalid callback name given, which must be a valid javascript function name");
+        }
+    }
+
     // ========================================================================
 
     // Used for checking origin or referer is an origin policy is enabled
     private String extractOriginOrReferer(HttpExchange pExchange) {
         Headers headers = pExchange.getRequestHeaders();
@@ -340,14 +351,15 @@ public class JolokiaHttpHandler implements HttpHandler {
 
     private void sendStreamingResponse(HttpExchange pExchange, ParsedUri pParsedUri, JSONStreamAware pJson) throws IOException {
         Headers headers = pExchange.getResponseHeaders();
         if (pJson != null) {
             headers.set("Content-Type", getMimeType(pParsedUri) + "; charset=utf-8");
-            String callback = pParsedUri.getParameter(ConfigKey.CALLBACK.getKeyValue());
             pExchange.sendResponseHeaders(200, 0);
             Writer writer = new OutputStreamWriter(pExchange.getResponseBody(), "UTF-8");
-            IoUtil.streamResponseAndClose(writer, pJson, callback);
+
+            String callback = pParsedUri.getParameter(ConfigKey.CALLBACK.getKeyValue());
+            IoUtil.streamResponseAndClose(writer, pJson, callback != null && MimeTypeUtil.isValidCallback(callback) ? callback : null);
         } else {
             headers.set("Content-Type", "text/plain");
             pExchange.sendResponseHeaders(200,-1);
         }
     }
@@ -358,11 +370,11 @@ public class JolokiaHttpHandler implements HttpHandler {
             Headers headers = pExchange.getResponseHeaders();
             if (pJson != null) {
                 headers.set("Content-Type", getMimeType(pParsedUri) + "; charset=utf-8");
                 String json = pJson.toJSONString();
                 String callback = pParsedUri.getParameter(ConfigKey.CALLBACK.getKeyValue());
-                String content = callback == null ? json : callback + "(" + json + ");";
+                String content = callback != null && MimeTypeUtil.isValidCallback(callback) ? callback + "(" + json + ");" : json;
                 byte[] response = content.getBytes("UTF8");
                 pExchange.sendResponseHeaders(200,response.length);
                 out = pExchange.getResponseBody();
                 out.write(response);
             } else {
@@ -378,20 +390,14 @@ public class JolokiaHttpHandler implements HttpHandler {
         }
     }
 
     // Get the proper mime type according to configuration
     private String getMimeType(ParsedUri pParsedUri) {
-        if (pParsedUri.getParameter(ConfigKey.CALLBACK.getKeyValue()) != null) {
-            return "text/javascript";
-        } else {
-            String mimeType = pParsedUri.getParameter(ConfigKey.MIME_TYPE.getKeyValue());
-            if (mimeType != null) {
-                return mimeType;
-            }
-            mimeType = configuration.get(ConfigKey.MIME_TYPE);
-            return mimeType != null ? mimeType : ConfigKey.MIME_TYPE.getDefaultValue();
-        }
+        return MimeTypeUtil.getResponseMimeType(
+            pParsedUri.getParameter(ConfigKey.MIME_TYPE.getKeyValue()),
+            configuration.get(ConfigKey.MIME_TYPE),
+            pParsedUri.getParameter(ConfigKey.CALLBACK.getKeyValue()));
     }
 
     // Creat a log handler from either the given class or by creating a default log handler printing
     // out to stderr
     private LogHandler createLogHandler(String pLogHandlerClass, String pDebug) {
diff --git a/agent/jvm/src/test/java/org/jolokia/jvmagent/handler/JolokiaHttpHandlerTest.java b/agent/jvm/src/test/java/org/jolokia/jvmagent/handler/JolokiaHttpHandlerTest.java
index bdeccd45..d69f48dc 100644
--- a/agent/jvm/src/test/java/org/jolokia/jvmagent/handler/JolokiaHttpHandlerTest.java
+++ b/agent/jvm/src/test/java/org/jolokia/jvmagent/handler/JolokiaHttpHandlerTest.java
@@ -72,18 +72,78 @@ public class JolokiaHttpHandlerTest {
         String result = out.toString("utf-8");
         assertTrue(result.endsWith("});"));
         assertTrue(result.startsWith("data({"));
     }
 
+
+    @Test
+    public void testInvalidMimeType() throws IOException, URISyntaxException {
+        checkMimeType("text/html", "text/plain");
+    }
+
+    @Test
+    public void testMimeTypeApplicationJson() throws IOException, URISyntaxException {
+        checkMimeType("application/json", "application/json");
+    }
+
+    private void checkMimeType(String given, String expected) throws IOException, URISyntaxException {
+        HttpExchange exchange = prepareExchange("http://localhost:8080/jolokia/read/java.lang:type=Memory/HeapMemoryUsage?mimeType=" + given);
+
+        // Simple GET method
+        expect(exchange.getRequestMethod()).andReturn("GET");
+
+        Headers header = new Headers();
+        ByteArrayOutputStream out = prepareResponse(handler, exchange, header);
+
+        handler.doHandle(exchange);
+
+        assertEquals(header.getFirst("content-type"),expected + "; charset=utf-8");
+    }
+
+    @Test
+    public void testInvalidCallbackGetStreaming() throws IOException, URISyntaxException, ParseException {
+        checkInvalidCallback(true);
+    }
+
+    @Test
+    public void testInvalidCallbackGetNonStreaming() throws IOException, URISyntaxException, ParseException {
+        checkInvalidCallback(false);
+    }
+
+    private void checkInvalidCallback(boolean streaming) throws URISyntaxException, IOException, ParseException {
+        JolokiaHttpHandler handler = new JolokiaHttpHandler(getConfig(ConfigKey.SERIALIZE_EXCEPTION, Boolean.toString(streaming)));
+        handler.start(false);
+
+        HttpExchange exchange = prepareExchange("http://localhost:8080/jolokia/read/java.lang:type=Memory/HeapMemoryUsage?callback=evilCallback();data");
+
+        // Simple GET method
+        expect(exchange.getRequestMethod()).andReturn("GET");
+
+        Headers header = new Headers();
+        ByteArrayOutputStream out = prepareResponse(handler, exchange, header);
+
+        handler.doHandle(exchange);
+
+        assertEquals(header.getFirst("content-type"),"text/plain; charset=utf-8");
+        String result = out.toString("utf-8");
+        JSONObject resp = (JSONObject) new JSONParser().parse(result);
+        assertTrue(resp.containsKey("error"));
+        assertEquals(resp.get("error_type"), IllegalArgumentException.class.getName());
+        assertTrue(((String) resp.get("error")).contains("callback"));
+        assertFalse(((String) resp.get("error")).contains("evilCallback"));
+
+        handler.stop();
+    }
+
+
     @Test
     public void testCallbackPost() throws URISyntaxException, IOException, java.text.ParseException {
         HttpExchange exchange = prepareExchange("http://localhost:8080/jolokia?callback=data",
                                                 "Content-Type","text/plain; charset=UTF-8",
                                                 "Origin",null
                                                );
 
-        // Simple GET method
         prepareMemoryPostReadRequest(exchange);
         Headers header = new Headers();
         ByteArrayOutputStream out = prepareResponse(handler, exchange, header);
 
         handler.doHandle(exchange);

Vulnerable code:             json = requestHandler.handleThrowable(
    // Extract mime type for response (if not JSONP)
    private String getMimeType(HttpServletRequest pReq) {
        String requestMimeType = pReq.getParameter(ConfigKey.MIME_TYPE.getKeyValue());
        if (requestMimeType != null) {
            return requestMimeType;
        }
        return configMimeType;
    }

        setContentType(pResp, callback != null ? "text/javascript" : getMimeType(pReq));
    public void initWithcustomAccessRestrictor() throws ServletException {
        expect(request.getParameter(ConfigKey.CALLBACK.getKeyValue())).andReturn(callback);
                expect(request.getParameter(ConfigKey.MIME_TYPE.getKeyValue())).andReturn("text/plain");
            String callback = pParsedUri.getParameter(ConfigKey.CALLBACK.getKeyValue());
            IoUtil.streamResponseAndClose(writer, pJson, callback);
                String content = callback == null ? json : callback + "(" + json + ");";
        if (pParsedUri.getParameter(ConfigKey.CALLBACK.getKeyValue()) != null) {
            return "text/javascript";
        } else {
            String mimeType = pParsedUri.getParameter(ConfigKey.MIME_TYPE.getKeyValue());
            if (mimeType != null) {
                return mimeType;
            }
            mimeType = configuration.get(ConfigKey.MIME_TYPE);
            return mimeType != null ? mimeType : ConfigKey.MIME_TYPE.getDefaultValue();
        }
        // Simple GET method
Fixed code: 
            // If a callback is given, check this is a valid javascript function name
            validateCallbackIfGiven(pReq);

            try {
                json = requestHandler.handleThrowable(
            } catch (Throwable exp2) {
                exp2.printStackTrace();
            }

        setContentType(pResp,
                       MimeTypeUtil.getResponseMimeType(
                           pReq.getParameter(ConfigKey.MIME_TYPE.getKeyValue()),
                           configMimeType, callback
                                                       ));
    private void validateCallbackIfGiven(HttpServletRequest pReq) {
        String callback = pReq.getParameter(ConfigKey.CALLBACK.getKeyValue());
        if (callback != null && !MimeTypeUtil.isValidCallback(callback)) {
            throw new IllegalArgumentException("Invalid callback name given, which must be a valid javascript function name");
        }
    }
package org.jolokia.util;

import java.util.regex.Pattern;

/**
 * Helper class for handling proper response mime types
 *
 * @author roland
 * @since 24.01.18
 */
public class MimeTypeUtil {


    /**
     * Extract the response mime type. This value is calculated for different situations:
     * <p>
     * <ul>
     * <li>If a callback is given and its valid, the mime type is "text/javascript"</li>
     * <li>Otherwise:
     * <ul>
     * <li>If a valid mimeType is given in the request ("text/plain", "application/json"), then this
     * mimet type is returned</li>
     * <li>If another mimeType is given, then "text/plain" is used</li>
     * <li>If no mimeType is given then a given default mime type is used, but also sanitized
     * as described above</li>
     * </ul>
     * </li>
     * </ul>
     *
     * @param pRequestMimeType the mimetype given in the request
     * @param defaultMimeType  the default mime type to use if none is given in the request
     * @param pCallback        a callback given (can be null)
     */
    public static String getResponseMimeType(String pRequestMimeType, String defaultMimeType, String pCallback) {

        // For a valid given callback, return "text/javascript" for proper inclusion
        if (pCallback != null && isValidCallback(pCallback)) {
            return "text/javascript";
        }

        // Pick up mime time from request, but sanitize
        if (pRequestMimeType != null) {
            return sanitize(pRequestMimeType);
        }

        // Use the given default mime type (possibly picked up from a configuration)
        return sanitize(defaultMimeType);
    }

    private static String sanitize(String mimeType) {
        for (String accepted : new String[]{
            "application/json",
            "text/plain"
        }) {
            if (accepted.equalsIgnoreCase(mimeType)) {
                return accepted;
            }
        }
        return "text/plain";
    }

    /**
     * Check that a callback matches a javascript function name. The argument must be not null
     *
     * @param pCallback callback to verify
     * @return true if valud, false otherwise
     */
    public static boolean isValidCallback(String pCallback) {
        Pattern validJavaScriptFunctionNamePattern =
            Pattern.compile("^[$A-Z_][0-9A-Z_$]*$", Pattern.CASE_INSENSITIVE);
        return validJavaScriptFunctionNamePattern.matcher(pCallback).matches();
    }

}
    public void initWithCustomAccessRestrictor() throws ServletException {
    @Test
    public void simpleGetWithWrongMimeType() throws ServletException, IOException {
        checkMimeTypes("text/html", "text/plain");
    }

    @Test
    public void simpleGetWithTextPlainMimeType() throws ServletException, IOException {
        checkMimeTypes("text/plain", "text/plain");
    }

    @Test
    public void simpleGetWithApplicationJsonMimeType() throws ServletException, IOException {
        checkMimeTypes("application/json", "application/json");
    }

    private void checkMimeTypes(String given, final String expected) throws ServletException, IOException {
        prepareStandardInitialisation();

        initRequestResponseMocks(
            getStandardRequestSetup(),
            new Runnable() {
                public void run() {
                    response.setCharacterEncoding("utf-8");
                    // The default content type
                    response.setContentType(expected);
                    response.setStatus(200);
                }
            });
        expect(request.getPathInfo()).andReturn(HttpTestUtil.HEAP_MEMORY_GET_REQUEST);
        expect(request.getParameter(ConfigKey.MIME_TYPE.getKeyValue())).andReturn(given);
        replay(request, response);

        servlet.doGet(request, response);

        verifyMocks();
        servlet.destroy();
    }

        expect(request.getParameter(ConfigKey.MIME_TYPE.getKeyValue())).andReturn(null);
    @Test
    public void withInvalidCallback() throws IOException, ServletException {
        servlet = new AgentServlet(new AllowAllRestrictor());
        initConfigMocks(null, null,"Error 400", IllegalArgumentException.class);
        replay(config, context);
        servlet.init(config);
        ByteArrayOutputStream sw = initRequestResponseMocks(
            "doSomethingEvil(); myCallback",
            getStandardRequestSetup(),
            getStandardResponseSetup());
        expect(request.getPathInfo()).andReturn(HttpTestUtil.HEAP_MEMORY_GET_REQUEST);
        expect(request.getAttribute("subject")).andReturn(null);
        expect(request.getParameter(ConfigKey.MIME_TYPE.getKeyValue())).andReturn(null);

        replay(request, response);

        servlet.doGet(request, response);
        String resp = sw.toString();
        assertTrue(resp.contains("error_type"));
        assertTrue(resp.contains("IllegalArgumentException"));
        assertTrue(resp.matches(".*status.*400.*"));
        servlet.destroy();
    }

        expect(request.getParameter(ConfigKey.CALLBACK.getKeyValue())).andReturn(callback).anyTimes();
                // The default content type
                expect(request.getParameter(ConfigKey.MIME_TYPE.getKeyValue())).andReturn("text/plain").anyTimes();
            // If a callback is given, check this is a valid javascript function name
            validateCallbackIfGiven(parsedUri);


    private void validateCallbackIfGiven(ParsedUri pUri) {
        String callback = pUri.getParameter(ConfigKey.CALLBACK.getKeyValue());
        if (callback != null && !MimeTypeUtil.isValidCallback(callback)) {
            throw new IllegalArgumentException("Invalid callback name given, which must be a valid javascript function name");
        }
    }


            String callback = pParsedUri.getParameter(ConfigKey.CALLBACK.getKeyValue());
            IoUtil.streamResponseAndClose(writer, pJson, callback != null && MimeTypeUtil.isValidCallback(callback) ? callback : null);
                String content = callback != null && MimeTypeUtil.isValidCallback(callback) ? callback + "(" + json + ");" : json;
        return MimeTypeUtil.getResponseMimeType(
            pParsedUri.getParameter(ConfigKey.MIME_TYPE.getKeyValue()),
            configuration.get(ConfigKey.MIME_TYPE),
            pParsedUri.getParameter(ConfigKey.CALLBACK.getKeyValue()));

    @Test
    public void testInvalidMimeType() throws IOException, URISyntaxException {
        checkMimeType("text/html", "text/plain");
    }

    @Test
    public void testMimeTypeApplicationJson() throws IOException, URISyntaxException {
        checkMimeType("application/json", "application/json");
    }

    private void checkMimeType(String given, String expected) throws IOException, URISyntaxException {
        HttpExchange exchange = prepareExchange("http://localhost:8080/jolokia/read/java.lang:type=Memory/HeapMemoryUsage?mimeType=" + given);

        // Simple GET method
        expect(exchange.getRequestMethod()).andReturn("GET");

        Headers header = new Headers();
        ByteArrayOutputStream out = prepareResponse(handler, exchange, header);

        handler.doHandle(exchange);

        assertEquals(header.getFirst("content-type"),expected + "; charset=utf-8");
    }

    @Test
    public void testInvalidCallbackGetStreaming() throws IOException, URISyntaxException, ParseException {
        checkInvalidCallback(true);
    }

    @Test
    public void testInvalidCallbackGetNonStreaming() throws IOException, URISyntaxException, ParseException {
        checkInvalidCallback(false);
    }

    private void checkInvalidCallback(boolean streaming) throws URISyntaxException, IOException, ParseException {
        JolokiaHttpHandler handler = new JolokiaHttpHandler(getConfig(ConfigKey.SERIALIZE_EXCEPTION, Boolean.toString(streaming)));
        handler.start(false);

        HttpExchange exchange = prepareExchange("http://localhost:8080/jolokia/read/java.lang:type=Memory/HeapMemoryUsage?callback=evilCallback();data");

        // Simple GET method
        expect(exchange.getRequestMethod()).andReturn("GET");

        Headers header = new Headers();
        ByteArrayOutputStream out = prepareResponse(handler, exchange, header);

        handler.doHandle(exchange);

        assertEquals(header.getFirst("content-type"),"text/plain; charset=utf-8");
        String result = out.toString("utf-8");
        JSONObject resp = (JSONObject) new JSONParser().parse(result);
        assertTrue(resp.containsKey("error"));
        assertEquals(resp.get("error_type"), IllegalArgumentException.class.getName());
        assertTrue(((String) resp.get("error")).contains("callback"));
        assertFalse(((String) resp.get("error")).contains("evilCallback"));

        handler.stop();
    }