Stored XSS in usememos/memos
Description
A stored cross-site scripting (XSS) vulnerability was discovered in usememos/memos version 0.9.1. This vulnerability allows an attacker to upload a JavaScript file containing a malicious script and reference it in an HTML file. When the HTML file is accessed, the malicious script is executed. This can lead to the theft of sensitive information, such as login credentials, from users visiting the affected website. The issue has been fixed in version 0.10.0.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A stored XSS vulnerability in usememos/memos 0.9.1 allows an attacker to upload a malicious JavaScript file referenced in an HTML file, leading to credential theft; fixed in 0.10.0.
Vulnerability
Analysis
CVE-2023-0109 is a stored cross-site scripting (XSS) vulnerability in usememos/memos version 0.9.1 [1]. The root cause lies in insufficient content-type validation when serving user-uploaded resources. An attacker can upload a JavaScript file containing a malicious script and then reference it in an HTML file. When another user accesses that HTML file, the script executes in their browser session [1][2].
Exploitation
The attack requires that the attacker can upload files to a memos instance (a standard feature for authenticated users) and then trick a victim into visiting the crafted HTML page. No additional authentication is needed as the uploaded resource is served by the application. The fix, implemented in commit 46c13a4 [2], adds multiple security headers (XSS-Protection, Content-Type nosniff) and forces text/plain content-type for resources with text or application MIME types, preventing script execution.
Impact
Successful exploitation allows an attacker to steal sensitive information, such as login credentials, from users who access the affected page [1]. This can lead to account takeover, data exposure, and further compromise of the memos instance.
Mitigation
The vulnerability was patched in version 0.10.0 [1]. Users of memos 0.9.1 should upgrade immediately. The memos project is an open-source, self-hosted note-taking tool [3], and instances not updated remain vulnerable. The Go vulnerability database also tracks this issue (GO-2024-3274) [4].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/usememos/memosGo | < 0.10.0 | 0.10.0 |
Affected products
3- ghsa-coords2 versionspkg:golang/github.com/usememos/memospkg:rpm/opensuse/govulncheck-vulndb&distro=openSUSE%20Tumbleweed
< 0.10.0+ 1 more
- (no CPE)range: < 0.10.0
- (no CPE)range: < 0.0.20241119T173509-1.1
- usememos/usememos/memosv5Range: unspecified
Patches
146c13a4b7f67chore: add skipper for secure (#913)
8 files changed · +74 −6
server/common.go+6 −0 modified@@ -1,6 +1,8 @@ package server import ( + "net/http" + "github.com/labstack/echo/v4" "github.com/usememos/memos/api" "github.com/usememos/memos/common" @@ -16,6 +18,10 @@ func composeResponse(data interface{}) response { } } +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()
server/resource.go+6 −1 modified@@ -7,6 +7,7 @@ import ( "net/http" "net/url" "strconv" + "strings" "time" "github.com/pkg/errors" @@ -266,7 +267,11 @@ func (s *Server) registerResourcePublicRoutes(g *echo.Group) { 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'")
server/server.go+7 −1 modified@@ -64,7 +64,13 @@ func NewServer(ctx context.Context, profile *profile.Profile) (*Server, error) { 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,
server/version/version.go+2 −3 modified@@ -7,10 +7,10 @@ import ( // 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" { @@ -29,7 +29,6 @@ func GetMinorVersion(version string) string { func GetSchemaVersion(version string) string { minorVersion := GetMinorVersion(version) - return minorVersion + ".0" }
server/version/version_test.go+33 −0 added@@ -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) + } + } +}
store/db/migration/prod/0.10/00__activity.sql+9 −0 added@@ -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 '{}' +);
store/db/migration/prod/LATEST__SCHEMA.sql+10 −0 modified@@ -93,3 +93,13 @@ CREATE TABLE tag ( 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 '{}' +);
web/src/components/EmbedMemoDialog.tsx+1 −1 modified@@ -34,7 +34,7 @@ const EmbedMemoDialog: React.FC<Props> = (props: Props) => { <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>
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5News mentions
0No linked articles in our index yet.