CVE-2018-1000129 (Java, published 2018-03-14T13:29Z)
Description: 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.
Severity: MEDIUM (CVSS v3 base score: 6.1)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
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.
Repository: https://github.com/rhuss/jolokia
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 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.


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();
    }