CVE-2023-0108: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Cross-site Scripting (XSS) - Stored in GitHub repository usememos/memos prior to 0.10.0.
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: 5.4
Published: 2023-01-07T04:15Z
Repository: https://github.com/usememos/memos
Language: Go
Files changed: server/common.go, server/resource.go, server/server.go, server/version/version.go, server/version/version_test.go, store/db/migration/prod/0.10/00__activity.sql, store/db/migration/prod/LATEST__SCHEMA.sql, web/src/components/EmbedMemoDialog.tsx
Commit date: 2023-01-07 10:51:34 +0800
Commit message: chore: add skipper for secure (#913)


Diff: diff --git a/server/common.go b/server/common.go
index 9c3b129..054fe6b 100644
--- a/server/common.go
+++ b/server/common.go
@@ -1,8 +1,10 @@
 package server
 
 import (
+	"net/http"
+
 	"github.com/labstack/echo/v4"
 	"github.com/usememos/memos/api"
 	"github.com/usememos/memos/common"
 )
 
@@ -14,10 +16,14 @@ func composeResponse(data interface{}) response {
 	return response{
 		Data: data,
 	}
 }
 
+func DefaultGetRequestSkipper(c echo.Context) bool {
+	return c.Request().Method == http.MethodGet
+}
+
 func (server *Server) DefaultAuthSkipper(c echo.Context) bool {
 	ctx := c.Request().Context()
 	path := c.Path()
 
 	// Skip auth.
diff --git a/server/resource.go b/server/resource.go
index 07ca84a..f46d1ec 100644
--- a/server/resource.go
+++ b/server/resource.go
@@ -5,10 +5,11 @@ import (
 	"fmt"
 	"io"
 	"net/http"
 	"net/url"
 	"strconv"
+	"strings"
 	"time"
 
 	"github.com/pkg/errors"
 	"github.com/usememos/memos/api"
 	"github.com/usememos/memos/common"
@@ -264,11 +265,15 @@ func (s *Server) registerResourcePublicRoutes(g *echo.Group) {
 		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)
 		}
 
-		c.Response().Writer.Header().Set("Content-Type", resource.Type)
+		if strings.HasPrefix(resource.Type, "text") || strings.HasPrefix(resource.Type, "application") {
+			c.Response().Writer.Header().Set("Content-Type", echo.MIMETextPlain)
+		} else {
+			c.Response().Writer.Header().Set("Content-Type", resource.Type)
+		}
 		c.Response().Writer.WriteHeader(http.StatusOK)
 		c.Response().Writer.Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
 		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 response").SetInternal(err)
diff --git a/server/server.go b/server/server.go
index af29982..9df7ce5 100644
--- a/server/server.go
+++ b/server/server.go
@@ -62,11 +62,17 @@ func NewServer(ctx context.Context, profile *profile.Profile) (*Server, error) {
 		TokenLookup: "cookie:_csrf",
 	}))
 
 	e.Use(middleware.CORS())
 
-	e.Use(middleware.Secure())
+	e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
+		Skipper:            DefaultGetRequestSkipper,
+		XSSProtection:      "1; mode=block",
+		ContentTypeNosniff: "nosniff",
+		XFrameOptions:      "SAMEORIGIN",
+		HSTSPreloadEnabled: false,
+	}))
 
 	e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
 		Skipper:      middleware.DefaultSkipper,
 		ErrorMessage: "Request timeout",
 		Timeout:      30 * time.Second,
diff --git a/server/version/version.go b/server/version/version.go
index 6f83fda..49021b4 100644
--- a/server/version/version.go
+++ b/server/version/version.go
@@ -5,14 +5,14 @@ import (
 	"strings"
 )
 
 // Version is the service current released version.
 // Semantic versioning: https://semver.org/
-var Version = "0.9.1"
+var Version = "0.10.0"
 
 // DevVersion is the service current development version.
-var DevVersion = "0.9.1"
+var DevVersion = "0.10.0"
 
 func GetCurrentVersion(mode string) string {
 	if mode == "dev" {
 		return DevVersion
 	}
@@ -27,11 +27,10 @@ func GetMinorVersion(version string) string {
 	return versionList[0] + "." + versionList[1]
 }
 
 func GetSchemaVersion(version string) string {
 	minorVersion := GetMinorVersion(version)
-
 	return minorVersion + ".0"
 }
 
 // convSemanticVersionToInt converts version string to int.
 func convSemanticVersionToInt(version string) int {
diff --git a/server/version/version_test.go b/server/version/version_test.go
new file mode 100644
index 0000000..21be7c9
--- /dev/null
+++ b/server/version/version_test.go
@@ -0,0 +1,33 @@
+package version
+
+import "testing"
+
+func TestIsVersionGreaterOrEqualThan(t *testing.T) {
+	tests := []struct {
+		version string
+		target  string
+		want    bool
+	}{
+		{
+			version: "0.9.1",
+			target:  "0.9.1",
+			want:    true,
+		},
+		{
+			version: "0.10.0",
+			target:  "0.9.1",
+			want:    true,
+		},
+		{
+			version: "0.9.0",
+			target:  "0.9.1",
+			want:    false,
+		},
+	}
+	for _, test := range tests {
+		result := IsVersionGreaterOrEqualThan(test.version, test.target)
+		if result != test.want {
+			t.Errorf("got result %v, want %v.", result, test.want)
+		}
+	}
+}
diff --git a/store/db/migration/prod/0.10/00__activity.sql b/store/db/migration/prod/0.10/00__activity.sql
new file mode 100644
index 0000000..0d4b05e
--- /dev/null
+++ b/store/db/migration/prod/0.10/00__activity.sql
@@ -0,0 +1,9 @@
+-- activity
+CREATE TABLE activity (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  creator_id INTEGER NOT NULL,
+  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
+  type TEXT NOT NULL DEFAULT '',
+  level TEXT NOT NULL CHECK (level IN ('INFO', 'WARN', 'ERROR')) DEFAULT 'INFO',
+  payload TEXT NOT NULL DEFAULT '{}'
+);
diff --git a/store/db/migration/prod/LATEST__SCHEMA.sql b/store/db/migration/prod/LATEST__SCHEMA.sql
index 3b7b4ad..7e94c60 100644
--- a/store/db/migration/prod/LATEST__SCHEMA.sql
+++ b/store/db/migration/prod/LATEST__SCHEMA.sql
@@ -91,5 +91,15 @@ CREATE TABLE memo_resource (
 CREATE TABLE tag (
   name TEXT NOT NULL,
   creator_id INTEGER NOT NULL,
   UNIQUE(name, creator_id)
 );
+
+-- activity
+CREATE TABLE activity (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  creator_id INTEGER NOT NULL,
+  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
+  type TEXT NOT NULL DEFAULT '',
+  level TEXT NOT NULL CHECK (level IN ('INFO', 'WARN', 'ERROR')) DEFAULT 'INFO',
+  payload TEXT NOT NULL DEFAULT '{}'
+);
diff --git a/web/src/components/EmbedMemoDialog.tsx b/web/src/components/EmbedMemoDialog.tsx
index f3d3c4f..aed984d 100644
--- a/web/src/components/EmbedMemoDialog.tsx
+++ b/web/src/components/EmbedMemoDialog.tsx
@@ -32,11 +32,11 @@ const EmbedMemoDialog: React.FC<Props> = (props: Props) => {
         <p className="text-base leading-6 mb-2">Copy and paste the below codes into your blog or website.</p>
         <pre className="w-full font-mono text-sm p-3 border rounded-lg">
           <code className="w-full break-all whitespace-pre-wrap">{memoEmbeddedCode()}</code>
         </pre>
         <p className="w-full text-sm leading-6 flex flex-row justify-between items-center mt-2">
-          * Only the public memo supports.
+          <span className="italic opacity-80">* Only the public memo supports.</span>
           <span className="btn-primary" onClick={handleCopyCode}>
             Copy
           </span>
         </p>
       </div>

Vulnerable code: 		c.Response().Writer.Header().Set("Content-Type", resource.Type)
	e.Use(middleware.Secure())
var Version = "0.9.1"
var DevVersion = "0.9.1"

          * Only the public memo supports.
Fixed code: 	"net/http"

func DefaultGetRequestSkipper(c echo.Context) bool {
	return c.Request().Method == http.MethodGet
}

	"strings"
		if strings.HasPrefix(resource.Type, "text") || strings.HasPrefix(resource.Type, "application") {
			c.Response().Writer.Header().Set("Content-Type", echo.MIMETextPlain)
		} else {
			c.Response().Writer.Header().Set("Content-Type", resource.Type)
		}
	e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
		Skipper:            DefaultGetRequestSkipper,
		XSSProtection:      "1; mode=block",
		ContentTypeNosniff: "nosniff",
		XFrameOptions:      "SAMEORIGIN",
		HSTSPreloadEnabled: false,
	}))
var Version = "0.10.0"
var DevVersion = "0.10.0"
package version

import "testing"

func TestIsVersionGreaterOrEqualThan(t *testing.T) {
	tests := []struct {
		version string
		target  string
		want    bool
	}{
		{
			version: "0.9.1",
			target:  "0.9.1",
			want:    true,
		},
		{
			version: "0.10.0",
			target:  "0.9.1",
			want:    true,
		},
		{
			version: "0.9.0",
			target:  "0.9.1",
			want:    false,
		},
	}
	for _, test := range tests {
		result := IsVersionGreaterOrEqualThan(test.version, test.target)
		if result != test.want {
			t.Errorf("got result %v, want %v.", result, test.want)
		}
	}
}
-- activity
CREATE TABLE activity (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  creator_id INTEGER NOT NULL,
  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
  type TEXT NOT NULL DEFAULT '',
  level TEXT NOT NULL CHECK (level IN ('INFO', 'WARN', 'ERROR')) DEFAULT 'INFO',
  payload TEXT NOT NULL DEFAULT '{}'
);

-- activity
CREATE TABLE activity (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  creator_id INTEGER NOT NULL,
  created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
  type TEXT NOT NULL DEFAULT '',
  level TEXT NOT NULL CHECK (level IN ('INFO', 'WARN', 'ERROR')) DEFAULT 'INFO',
  payload TEXT NOT NULL DEFAULT '{}'
);
          <span className="italic opacity-80">* Only the public memo supports.</span>