From 30fea0ca6a93cfe5f1131779bbd5d9e57cd85fc5 Mon Sep 17 00:00:00 2001 From: cavidelizade Date: Fri, 17 Jul 2026 01:16:53 +0400 Subject: [PATCH] fix(api): download attachments instead of rendering them inline Attachment uploads don't validate content-type, and ServeFile streamed every object back with Content-Disposition: inline. A member could upload an HTML file (or an SVG with script) and have it execute on the API origin when another member opened the attachment URL. Force Content-Disposition: attachment for the attachments/ prefix so the browser downloads rather than renders it. The uploads/ prefix (avatars, covers, logos) is validated as images and stays inline. Closes #328 Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/api/internal/handler/upload.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/api/internal/handler/upload.go b/apps/api/internal/handler/upload.go index 9436fc1..f2de2c3 100644 --- a/apps/api/internal/handler/upload.go +++ b/apps/api/internal/handler/upload.go @@ -187,6 +187,14 @@ func (h *UploadHandler) ServeFile(c *gin.Context) { c.Header("Content-Type", info.ContentType) c.Header("X-Content-Type-Options", "nosniff") - c.Header("Content-Disposition", "inline") + // Attachments are arbitrary user files with an unvalidated content-type, so + // force a download instead of rendering them inline: an uploaded .html or + // SVG would otherwise execute on the API origin when opened. The uploads/ + // prefix (avatars, covers, logos) is validated as images and stays inline. + disposition := "inline" + if strings.HasPrefix(path, "attachments/") { + disposition = "attachment" + } + c.Header("Content-Disposition", disposition) c.DataFromReader(http.StatusOK, info.Size, info.ContentType, obj, nil) }