CVE-2022-25978: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: All versions of the package github.com/usememos/memos/server are vulnerable to Cross-site Scripting (XSS) due to insufficient checks on external resources, which allows malicious actors to introduce links starting with a javascript: scheme. 
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: 2023-02-15T05:15Z
Repository: https://github.com/usememos/memos
Language: Other
Files changed: server/resource.go, web/src/components/CreateResourceDialog.tsx
Commit date: 2023-02-11 17:34:29 +0800
Commit message: chore: validate external link (#1069)


Diff: diff --git a/server/resource.go b/server/resource.go
index f951de3..3d79b0a 100644
--- a/server/resource.go
+++ b/server/resource.go
@@ -36,10 +36,14 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
 		if err := json.NewDecoder(c.Request().Body).Decode(resourceCreate); err != nil {
 			return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post resource request").SetInternal(err)
 		}
 
 		resourceCreate.CreatorID = userID
+		// Only allow those external links with http prefix.
+		if resourceCreate.ExternalLink != "" && !strings.HasPrefix(resourceCreate.ExternalLink, "http") {
+			return echo.NewHTTPError(http.StatusBadRequest, "Invalid external link")
+		}
 		resource, err := s.Store.CreateResource(ctx, resourceCreate)
 		if err != nil {
 			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err)
 		}
 		if err := s.createResourceCreateActivity(c, resource); err != nil {
@@ -186,17 +190,11 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
 		resource, err := s.Store.FindResource(ctx, resourceFind)
 		if err != nil {
 			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource").SetInternal(err)
 		}
 
-		c.Response().Writer.WriteHeader(http.StatusOK)
-		c.Response().Writer.Header().Set("Content-Type", resource.Type)
-		c.Response().Writer.Header().Set(echo.HeaderContentSecurityPolicy, "default-src 'self'")
-		if _, err := c.Response().Writer.Write(resource.Blob); err != nil {
-			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to write resource blob").SetInternal(err)
-		}
-		return nil
+		return c.Stream(http.StatusOK, resource.Type, bytes.NewReader(resource.Blob))
 	})
 
 	g.PATCH("/resource/:resourceId", func(c echo.Context) error {
 		ctx := c.Request().Context()
 		userID, ok := c.Get(getUserIDContextKey()).(int)
@@ -294,20 +292,19 @@ func (s *Server) registerResourcePublicRoutes(g *echo.Group) {
 			Filename: &filename,
 			GetBlob:  true,
 		}
 		resource, err := s.Store.FindResource(ctx, resourceFind)
 		if err != nil {
-			return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to fetch resource ID: %v", resourceID)).SetInternal(err)
+			return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find resource by ID: %v", resourceID)).SetInternal(err)
 		}
 
-		resourceType := strings.ToLower(resource.Type)
-		if strings.HasPrefix(resourceType, "text") || (strings.HasPrefix(resourceType, "application") && resourceType != "application/pdf") {
-			resourceType = echo.MIMETextPlain
-		}
 		c.Response().Writer.Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
 		c.Response().Writer.Header().Set(echo.HeaderContentSecurityPolicy, "default-src 'self'")
-		if strings.HasPrefix(resourceType, "video") || strings.HasPrefix(resourceType, "audio") {
+		resourceType := strings.ToLower(resource.Type)
+		if strings.HasPrefix(resourceType, "text") {
+			resourceType = echo.MIMETextPlainCharsetUTF8
+		} else if strings.HasPrefix(resourceType, "video") || strings.HasPrefix(resourceType, "audio") {
 			http.ServeContent(c.Response(), c.Request(), resource.Filename, time.Unix(resource.UpdatedTs, 0), bytes.NewReader(resource.Blob))
 			return nil
 		}
 		return c.Stream(http.StatusOK, resourceType, bytes.NewReader(resource.Blob))
 	})
diff --git a/web/src/components/CreateResourceDialog.tsx b/web/src/components/CreateResourceDialog.tsx
index ec80ea3..6d5e365 100644
--- a/web/src/components/CreateResourceDialog.tsx
+++ b/web/src/components/CreateResourceDialog.tsx
@@ -192,11 +192,11 @@ const CreateResourceDialog: React.FC<Props> = (props: Props) => {
             <Typography className="!mb-1" level="body2">
               Link
             </Typography>
             <Input
               className="mb-2"
-              placeholder="File link"
+              placeholder="https://the.link.to/your/resource"
               value={resourceCreate.externalLink}
               onChange={handleExternalLinkChanged}
               fullWidth
             />
             <Typography className="!mb-1" level="body2">

Vulnerable code: 		c.Response().Writer.WriteHeader(http.StatusOK)
		c.Response().Writer.Header().Set("Content-Type", resource.Type)
		c.Response().Writer.Header().Set(echo.HeaderContentSecurityPolicy, "default-src 'self'")
		if _, err := c.Response().Writer.Write(resource.Blob); err != nil {
			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to write resource blob").SetInternal(err)
		}
		return nil
			return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to fetch resource ID: %v", resourceID)).SetInternal(err)
		resourceType := strings.ToLower(resource.Type)
		if strings.HasPrefix(resourceType, "text") || (strings.HasPrefix(resourceType, "application") && resourceType != "application/pdf") {
			resourceType = echo.MIMETextPlain
		}
		if strings.HasPrefix(resourceType, "video") || strings.HasPrefix(resourceType, "audio") {
              placeholder="File link"
Fixed code: 		// Only allow those external links with http prefix.
		if resourceCreate.ExternalLink != "" && !strings.HasPrefix(resourceCreate.ExternalLink, "http") {
			return echo.NewHTTPError(http.StatusBadRequest, "Invalid external link")
		}
		return c.Stream(http.StatusOK, resource.Type, bytes.NewReader(resource.Blob))
			return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find resource by ID: %v", resourceID)).SetInternal(err)
		resourceType := strings.ToLower(resource.Type)
		if strings.HasPrefix(resourceType, "text") {
			resourceType = echo.MIMETextPlainCharsetUTF8
		} else if strings.HasPrefix(resourceType, "video") || strings.HasPrefix(resourceType, "audio") {
              placeholder="https://the.link.to/your/resource"