CVE-2023-41318: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: matrix-media-repo is a highly customizable multi-domain media repository for the Matrix chat ecosystem. In affected versions an attacker could upload a malicious piece of media to the media repo, which would then be served with `Content-Disposition: inline` upon download. This vulnerability could be leveraged to execute scripts embedded in SVG content. Commits `77ec235` and `bf8abdd` fix the issue and are included in the 1.3.0 release. Operators should upgrade to v1.3.0 as soon as possible. Operators unable to upgrade should override the `Content-Disposition` header returned by matrix-media-repo as a workaround.
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-09-08T20:15Z
Repository: https://github.com/turt2live/matrix-media-repo
Language: Go
Files changed: api/_routers/98-use-rcontext.go
Commit date: 2023-09-05 16:14:13 -0600
Commit message: Sniff content type during download for disposition

Diff: diff --git a/api/_routers/98-use-rcontext.go b/api/_routers/98-use-rcontext.go
index 18c678c..510db1f 100644
--- a/api/_routers/98-use-rcontext.go
+++ b/api/_routers/98-use-rcontext.go
@@ -13,15 +13,17 @@ import (
 	"net/url"
 	"strconv"
 	"strings"
 
 	"github.com/alioygur/is"
+	"github.com/gabriel-vasile/mimetype"
 	"github.com/getsentry/sentry-go"
 	"github.com/turt2live/matrix-media-repo/api/_responses"
 	"github.com/turt2live/matrix-media-repo/common"
 	"github.com/turt2live/matrix-media-repo/common/rcontext"
 	"github.com/turt2live/matrix-media-repo/util"
+	"github.com/turt2live/matrix-media-repo/util/readers"
 )
 
 type GeneratorFn = func(r *http.Request, ctx rcontext.RequestContext) interface{}
 
 type RContextRouter struct {
@@ -92,24 +94,41 @@ beforeParseDownload:
 			res = _responses.BadRequest(rangeErrMsg)
 			doRange = false
 			goto beforeParseDownload // reprocess `res`
 		}
 
-		contentType = downloadRes.ContentType
+		contentType = "application/octet-stream"
 		expectedBytes = downloadRes.SizeBytes
 
+		// Don't rely on user-supplied values for content-type
+		br := readers.NewBufferReadsReader(downloadRes.Data)
+		if mimeType, err := mimetype.DetectReader(br); err != nil {
+			rctx.Log.Warn("Non-fatal error sniffing mime type of download: ", err)
+			sentry.CaptureException(err)
+		} else if mimeType != nil {
+			contentType = mimeType.String()
+		}
+		ogReader := downloadRes.Data
+		downloadRes.Data = readers.NewCancelCloser(io.NopCloser(br.GetRewoundReader()), func() {
+			_ = ogReader.Close()
+		})
+
+		if contentType != downloadRes.ContentType {
+			rctx.Log.Debugf("Expected '%s' content type but ended up with '%s'", downloadRes.ContentType, contentType)
+		}
+
 		if shouldCache {
 			headers.Set("Cache-Control", "private, max-age=259200") // 3 days
 		}
 
 		if downloadRes.SizeBytes > 0 {
 			headers.Set("Accept-Ranges", "bytes")
 		}
 
 		disposition := downloadRes.TargetDisposition
 		if disposition == "" {
-			disposition = "inline"
+			disposition = "attachment"
 		} else if disposition == "infer" {
 			if contentType == "" {
 				disposition = "attachment"
 			} else {
 				if util.CanInline(contentType) {

Vulnerable code: 		contentType = downloadRes.ContentType
			disposition = "inline"
Fixed code: 	"github.com/gabriel-vasile/mimetype"
	"github.com/turt2live/matrix-media-repo/util/readers"
		contentType = "application/octet-stream"
		// Don't rely on user-supplied values for content-type
		br := readers.NewBufferReadsReader(downloadRes.Data)
		if mimeType, err := mimetype.DetectReader(br); err != nil {
			rctx.Log.Warn("Non-fatal error sniffing mime type of download: ", err)
			sentry.CaptureException(err)
		} else if mimeType != nil {
			contentType = mimeType.String()
		}
		ogReader := downloadRes.Data
		downloadRes.Data = readers.NewCancelCloser(io.NopCloser(br.GetRewoundReader()), func() {
			_ = ogReader.Close()
		})

		if contentType != downloadRes.ContentType {
			rctx.Log.Debugf("Expected '%s' content type but ended up with '%s'", downloadRes.ContentType, contentType)
		}

			disposition = "attachment"