From fae3251dfcaa405ef9d19ce4ede38a12562d8461 Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Fri, 25 Sep 2026 11:52:31 +0530 Subject: [PATCH 1/4] stop uploaded SVG files from running scripts An upload could send a file type like "image/svg+xml; x". We saved that type as sent, so it skipped the SVG check and the file opened in the tab with scripts on. Types ending in +xml had the same hole. We now clean up the type before saving it and again when serving it, which also fixes files uploaded before this change. Files served from libredesk get scripts turned off, except PDFs, because Chrome's PDF viewer won't load that way. SVG and other XML types always download. On S3, uploads store the cleaned type and a download or inline header on the file. --- cmd/media.go | 20 ++-- cmd/media_test.go | 57 +++++++++++ internal/media/media.go | 21 +---- internal/media/models/models.go | 30 +++++- internal/media/models/models_test.go | 57 +++++++++++ internal/media/stores/s3/s3.go | 11 ++- internal/media/stores/s3/s3_test.go | 136 +++++++++++++++++++++++++++ 7 files changed, 296 insertions(+), 36 deletions(-) create mode 100644 internal/media/models/models_test.go create mode 100644 internal/media/stores/s3/s3_test.go diff --git a/cmd/media.go b/cmd/media.go index fb1234b0..8a0e44a6 100644 --- a/cmd/media.go +++ b/cmd/media.go @@ -255,22 +255,18 @@ func serveMediaFile(r *fastglue.Request, app *App, uuid string, media *mmodels.M consts := app.consts.Load().(*constants) switch consts.UploadProvider { case "fs": - disposition := "attachment" - - // Inline images/videos/pdfs. SVG excluded. - if !forceDownload && - media.ContentType != "image/svg+xml" && - (strings.HasPrefix(media.ContentType, "image/") || - strings.HasPrefix(media.ContentType, "video/") || - media.ContentType == "application/pdf") { - disposition = "inline" + // Older rows hold content types exactly as the client sent them. + contentType := mmodels.NormalizeContentType(media.ContentType) + disposition := mmodels.ContentDisposition(contentType) + if forceDownload { + disposition = mmodels.DispositionAttachment } - r.RequestCtx.Response.Header.Set("Content-Type", media.ContentType) + r.RequestCtx.Response.Header.Set("Content-Type", contentType) r.RequestCtx.Response.Header.Set("Content-Disposition", mime.FormatMediaType(disposition, map[string]string{"filename": media.Filename})) r.RequestCtx.Response.Header.Set("X-Content-Type-Options", "nosniff") - // Sandbox SVGs. - if media.ContentType == "image/svg+xml" { + // Chrome's PDF viewer does not load in a sandboxed page. + if contentType != mmodels.ContentTypePDF { r.RequestCtx.Response.Header.Set("Content-Security-Policy", "sandbox") } r.RequestCtx.Response.Header.Set("Cache-Control", fmt.Sprintf("%s, max-age=%d, immutable", cacheVisibility(media.Private), int(mediaCacheTTL.Seconds()))) diff --git a/cmd/media_test.go b/cmd/media_test.go index 3cc8f86a..bbb6bcf1 100644 --- a/cmd/media_test.go +++ b/cmd/media_test.go @@ -3,7 +3,14 @@ package main import ( "bytes" "encoding/json" + "os" + "path/filepath" + "strings" "testing" + + mmodels "github.com/abhinavxd/libredesk/internal/media/models" + "github.com/valyala/fasthttp" + "github.com/zerodha/fastglue" ) func TestPrepareImageUploadAllowsOversizedImageWithoutThumbnail(t *testing.T) { @@ -31,3 +38,53 @@ func TestPrepareImageUploadAllowsOversizedImageWithoutThumbnail(t *testing.T) { t.Fatalf("dimensions = %dx%d, want 10000x5000", meta.Width, meta.Height) } } + +func TestServeMediaFileSandboxesAllButPDF(t *testing.T) { + const uuid = "0b7a3c1e-2f4d-4b8a-9c6e-1d2f3a4b5c6d" + dir := t.TempDir() + svg := []byte(``) + if err := os.WriteFile(filepath.Join(dir, uuid), svg, 0o600); err != nil { + t.Fatal(err) + } + if err := ko.Set("upload.fs.upload_path", dir); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { ko.Delete("upload.fs.upload_path") }) + + app := &App{} + app.consts.Store(&constants{UploadProvider: "fs"}) + + tests := []struct { + contentType string + wantType string + wantDisposition string + wantSandbox bool + }{ + {"image/svg+xml", "image/svg+xml", "attachment", true}, + {"image/svg+xml; x", "image/svg+xml", "attachment", true}, + {"image/x+xml", "image/x+xml", "attachment", true}, + {"text/plain,image/svg+xml", "application/octet-stream", "attachment", true}, + {"image/png", "image/png", "inline", true}, + {"application/pdf", "application/pdf", "inline", false}, + } + for _, tt := range tests { + t.Run(tt.contentType, func(t *testing.T) { + req := &fastglue.Request{RequestCtx: &fasthttp.RequestCtx{}, Context: app} + media := &mmodels.Media{ContentType: tt.contentType, Filename: "file", Private: true} + if err := serveMediaFile(req, app, uuid, media); err != nil { + t.Fatal(err) + } + + h := &req.RequestCtx.Response.Header + if got := string(h.ContentType()); got != tt.wantType { + t.Fatalf("Content-Type = %q, want %q", got, tt.wantType) + } + if got := string(h.Peek("Content-Disposition")); !strings.HasPrefix(got, tt.wantDisposition) { + t.Fatalf("Content-Disposition = %q, want %s", got, tt.wantDisposition) + } + if got := string(h.Peek("Content-Security-Policy")) == "sandbox"; got != tt.wantSandbox { + t.Fatalf("sandboxed = %v, want %v", got, tt.wantSandbox) + } + }) + } +} diff --git a/internal/media/media.go b/internal/media/media.go index af1ce5b0..04f180d4 100644 --- a/internal/media/media.go +++ b/internal/media/media.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "io" - "mime" "net/http" "net/url" "os" @@ -260,15 +259,7 @@ func (m *Manager) GetBlob(name string) ([]byte, error) { // GetURL returns the URL for accessing a media file by its name. func (m *Manager) GetURL(uuid, contentType, fileName string) string { - // Keep some content types inline. SVG excluded. - disposition := "attachment" - if contentType != "image/svg+xml" && - (strings.HasPrefix(contentType, "image/") || - strings.HasPrefix(contentType, "video/") || - contentType == "application/pdf") { - disposition = "inline" - } - return m.store.GetURL(uuid, disposition, fileName) + return m.store.GetURL(uuid, models.ContentDisposition(contentType), fileName) } func (m *Manager) GetURLForDownload(uuid, fileName string) string { @@ -415,15 +406,7 @@ func (m *Manager) deleteUnlinkedRows(stmt *sqlx.Stmt) error { // For generic types, it uses http.DetectContentType (stdlib) as a fast path, // falling back to mimetype library for deeper inspection using magic numbers. func (m *Manager) detectContentType(sourceContentType string, content io.ReadSeeker) (string, error) { - // Set default if empty - if sourceContentType == "" { - sourceContentType = "application/octet-stream" - } - - // Handle "image/svg+xml; charset=utf-8", keep just the type. - if mediaType, _, err := mime.ParseMediaType(sourceContentType); err == nil && mediaType != "" { - sourceContentType = mediaType - } + sourceContentType = models.NormalizeContentType(sourceContentType) // Trust source unless it's a generic/useless type if sourceContentType != "application/octet-stream" && diff --git a/internal/media/models/models.go b/internal/media/models/models.go index 4d63e4d0..e2e3ceb4 100644 --- a/internal/media/models/models.go +++ b/internal/media/models/models.go @@ -2,6 +2,9 @@ package models import ( "encoding/json" + "errors" + "mime" + "strings" "time" "github.com/volatiletech/null/v9" @@ -13,7 +16,11 @@ const ( ModelUser = "users" ModelHelpArticles = "help_articles" - DispositionInline = "inline" + DispositionInline = "inline" + DispositionAttachment = "attachment" + + ContentTypeOctetStream = "application/octet-stream" + ContentTypePDF = "application/pdf" ) // IsPublicModel reports whether media linked to the model type is served without authentication. @@ -42,3 +49,24 @@ type Media struct { URL string `json:"url"` Content []byte `json:"-"` } + +// NormalizeContentType returns the lowercased type/subtype of a Content-Type value, or application/octet-stream if it does not parse. +func NormalizeContentType(contentType string) string { + mediaType, _, err := mime.ParseMediaType(contentType) + if (err != nil && !errors.Is(err, mime.ErrInvalidMediaParameter)) || !strings.Contains(mediaType, "/") { + return ContentTypeOctetStream + } + return mediaType +} + +// ContentDisposition returns inline for images, video and PDF. XML types such as SVG can run scripts and are always attachments. +func ContentDisposition(contentType string) string { + contentType = NormalizeContentType(contentType) + if strings.HasSuffix(contentType, "+xml") { + return DispositionAttachment + } + if strings.HasPrefix(contentType, "image/") || strings.HasPrefix(contentType, "video/") || contentType == ContentTypePDF { + return DispositionInline + } + return DispositionAttachment +} diff --git a/internal/media/models/models_test.go b/internal/media/models/models_test.go new file mode 100644 index 00000000..3e29626d --- /dev/null +++ b/internal/media/models/models_test.go @@ -0,0 +1,57 @@ +package models + +import "testing" + +func TestNormalizeContentType(t *testing.T) { + tests := []struct { + in string + want string + }{ + {"image/png", "image/png"}, + {"IMAGE/SVG+XML", "image/svg+xml"}, + {"image/svg+xml; charset=utf-8", "image/svg+xml"}, + {"image/svg+xml; x", "image/svg+xml"}, + {"Image/Svg+Xml; x", "image/svg+xml"}, + {`image/svg+xml ; a="b`, "image/svg+xml"}, + {"text/plain,image/svg+xml", ContentTypeOctetStream}, + {"image", ContentTypeOctetStream}, + {"", ContentTypeOctetStream}, + {"garbage", ContentTypeOctetStream}, + } + for _, tt := range tests { + t.Run(tt.in, func(t *testing.T) { + if got := NormalizeContentType(tt.in); got != tt.want { + t.Fatalf("NormalizeContentType(%q) = %q, want %q", tt.in, got, tt.want) + } + }) + } +} + +func TestContentDisposition(t *testing.T) { + tests := []struct { + in string + want string + }{ + {"image/png", DispositionInline}, + {"video/mp4", DispositionInline}, + {"application/pdf", DispositionInline}, + {"Application/PDF; x", DispositionInline}, + {"image/svg+xml", DispositionAttachment}, + {"image/svg+xml; x", DispositionAttachment}, + {"IMAGE/SVG+XML", DispositionAttachment}, + {"image/x+xml", DispositionAttachment}, + {"video/x+xml", DispositionAttachment}, + {"application/xhtml+xml", DispositionAttachment}, + {"text/html", DispositionAttachment}, + {"text/xml", DispositionAttachment}, + {"text/plain,image/svg+xml", DispositionAttachment}, + {"", DispositionAttachment}, + } + for _, tt := range tests { + t.Run(tt.in, func(t *testing.T) { + if got := ContentDisposition(tt.in); got != tt.want { + t.Fatalf("ContentDisposition(%q) = %q, want %q", tt.in, got, tt.want) + } + }) + } +} diff --git a/internal/media/stores/s3/s3.go b/internal/media/stores/s3/s3.go index db8ad0f1..8a8d43d8 100644 --- a/internal/media/stores/s3/s3.go +++ b/internal/media/stores/s3/s3.go @@ -12,6 +12,7 @@ import ( "time" "github.com/abhinavxd/libredesk/internal/media" + "github.com/abhinavxd/libredesk/internal/media/models" "github.com/rhnvrm/simples3" ) @@ -67,11 +68,13 @@ func New(opt Opt) (media.Store, error) { // Put uploads a file to S3 with the specified name, content type, and file content. // It returns the name of the file or an error if the upload fails. func (c *Client) Put(name string, cType string, file io.ReadSeeker) (string, error) { + cType = models.NormalizeContentType(cType) p := simples3.UploadInput{ - Bucket: c.opts.Bucket, - ContentType: cType, - FileName: name, - Body: file, + Bucket: c.opts.Bucket, + ContentType: cType, + ContentDisposition: models.ContentDisposition(cType), + FileName: name, + Body: file, // Paths inside the bucket should not start with /. ObjectKey: c.makeBucketPath(name), } diff --git a/internal/media/stores/s3/s3_test.go b/internal/media/stores/s3/s3_test.go new file mode 100644 index 00000000..23f6e259 --- /dev/null +++ b/internal/media/stores/s3/s3_test.go @@ -0,0 +1,136 @@ +package s3 + +import ( + "io" + "mime" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/abhinavxd/libredesk/internal/media/models" + "github.com/google/uuid" +) + +func TestPutStoresSafeContentHeaders(t *testing.T) { + tests := []struct { + contentType string + wantType string + disposition string + }{ + {"image/svg+xml", "image/svg+xml", "attachment"}, + {"IMAGE/SVG+XML; x", "image/svg+xml", "attachment"}, + {`image/svg+xml; charset="unterminated`, "image/svg+xml", "attachment"}, + {"image/svg+xml; charset=utf-8; charset=ascii", "application/octet-stream", "attachment"}, + {"text/plain,image/svg+xml", "application/octet-stream", "attachment"}, + {"image/x+xml", "image/x+xml", "attachment"}, + {"text/html", "text/html", "attachment"}, + {"", "application/octet-stream", "attachment"}, + {"image/png", "image/png", "inline"}, + {"video/mp4", "video/mp4", "inline"}, + {"application/pdf", "application/pdf", "inline"}, + } + for _, tt := range tests { + t.Run(tt.contentType, func(t *testing.T) { + body := `` + requests := make(chan http.Header, 1) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut || r.URL.Path != "/test-bucket/uploads/file" { + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + } + got, err := io.ReadAll(r.Body) + if err != nil || string(got) != body { + t.Errorf("uploaded body = %q, error = %v", got, err) + } + requests <- r.Header.Clone() + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + store, err := New(Opt{ + URL: server.URL, + PublicURL: "https://cdn.example.com", + AccessKey: uuid.NewString(), + SecretKey: uuid.NewString(), + Region: "test-region", + Bucket: "test-bucket", + BucketPath: "uploads", + BucketType: "private", + }) + if err != nil { + t.Fatal(err) + } + if _, err := store.Put("file", tt.contentType, strings.NewReader(body)); err != nil { + t.Fatal(err) + } + + headers := <-requests + if got := headers.Get("Content-Type"); got != tt.wantType { + t.Errorf("Content-Type = %q, want %q", got, tt.wantType) + } + if got := headers.Get("Content-Disposition"); got != tt.disposition { + t.Errorf("Content-Disposition = %q, want %q", got, tt.disposition) + } + }) + } +} + +func TestGetURLPreservesPublicURLs(t *testing.T) { + tests := []struct { + name string + bucketType string + publicURL string + disposition string + wantHost string + wantPath string + wantSigned bool + }{ + {"private download", "private", "", models.DispositionAttachment, "storage.example.com", "/test-bucket/uploads/existing-file", true}, + {"private inline", "private", "", models.DispositionInline, "storage.example.com", "/test-bucket/uploads/existing-file", true}, + {"private custom download", "private", "https://cdn.example.com", models.DispositionAttachment, "cdn.example.com", "/uploads/existing-file", false}, + {"private custom inline", "private", "https://cdn.example.com", models.DispositionInline, "cdn.example.com", "/uploads/existing-file", false}, + {"public download", "public", "", models.DispositionAttachment, "storage.example.com", "/test-bucket/uploads/existing-file", false}, + {"public inline", "public", "", models.DispositionInline, "storage.example.com", "/test-bucket/uploads/existing-file", false}, + {"public custom download", "public", "https://cdn.example.com", models.DispositionAttachment, "cdn.example.com", "/uploads/existing-file", false}, + {"public custom inline", "public", "https://cdn.example.com", models.DispositionInline, "cdn.example.com", "/uploads/existing-file", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + store, err := New(Opt{ + URL: "https://storage.example.com", + PublicURL: tt.publicURL, + AccessKey: uuid.NewString(), + SecretKey: uuid.NewString(), + Region: "test-region", + Bucket: "test-bucket", + BucketPath: "uploads", + BucketType: tt.bucketType, + }) + if err != nil { + t.Fatal(err) + } + filename := "customer attachment.svg" + u, err := url.Parse(store.GetURL("existing-file", tt.disposition, filename)) + if err != nil { + t.Fatal(err) + } + + if u.Host != tt.wantHost || u.Path != tt.wantPath { + t.Errorf("URL target = %s%s, want %s%s", u.Host, u.Path, tt.wantHost, tt.wantPath) + } + query := u.Query() + if got := query.Get("X-Amz-Signature") != ""; got != tt.wantSigned { + t.Errorf("signed URL = %v, want %v", got, tt.wantSigned) + } + if tt.wantSigned { + got, params, err := mime.ParseMediaType(query.Get("response-content-disposition")) + if err != nil || got != tt.disposition || params["filename"] != filename { + t.Errorf("download disposition = %q, filename = %q, error = %v", got, params["filename"], err) + } + } else if u.RawQuery != "" { + t.Error("public URL has unexpected query parameters") + } + }) + } +} From dc516bdd203f4f2ad810b05860181e7413ae3163 Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Fri, 25 Sep 2026 18:36:03 +0530 Subject: [PATCH 2/4] refactor: replace string literals with constants for content disposition --- internal/media/media.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/media/media.go b/internal/media/media.go index 04f180d4..17c68229 100644 --- a/internal/media/media.go +++ b/internal/media/media.go @@ -263,7 +263,7 @@ func (m *Manager) GetURL(uuid, contentType, fileName string) string { } func (m *Manager) GetURLForDownload(uuid, fileName string) string { - return m.store.GetURL(uuid, "attachment", fileName) + return m.store.GetURL(uuid, models.DispositionAttachment, fileName) } // GetSignedURL generates a signed URL for secure media access if the store supports it. @@ -290,7 +290,7 @@ func (m *Manager) GetThumbnailURL(uuid string) string { return u.String() } } - return m.store.GetURL(image.ThumbPrefix+uuid, "inline", "") + return m.store.GetURL(image.ThumbPrefix+uuid, models.DispositionInline, "") } // SignedURLValidator returns the store's signature validator if available. @@ -409,7 +409,7 @@ func (m *Manager) detectContentType(sourceContentType string, content io.ReadSee sourceContentType = models.NormalizeContentType(sourceContentType) // Trust source unless it's a generic/useless type - if sourceContentType != "application/octet-stream" && + if sourceContentType != models.ContentTypeOctetStream && sourceContentType != "application/data" && sourceContentType != "application/binary" { m.lo.Debug("detected media content type from trusted source", "detected_type", sourceContentType) @@ -426,7 +426,7 @@ func (m *Manager) detectContentType(sourceContentType string, content io.ReadSee // If stdlib gives a useful type, use it. // stdlib defaults to application/octet-stream for unknown types. - if detected != "application/octet-stream" { + if detected != models.ContentTypeOctetStream { content.Seek(0, io.SeekStart) m.lo.Debug("detected media content type using stdlib", "detected_type", detected, "source_type", sourceContentType) return detected, nil From 8272aa2d07277d00ebafd64105ef5837bc380670 Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Fri, 25 Sep 2026 18:52:50 +0530 Subject: [PATCH 3/4] add forced download cases to the media serve test --- cmd/media_test.go | 21 ++++++++++++++------- internal/media/media.go | 2 +- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/cmd/media_test.go b/cmd/media_test.go index bbb6bcf1..f6330454 100644 --- a/cmd/media_test.go +++ b/cmd/media_test.go @@ -55,21 +55,28 @@ func TestServeMediaFileSandboxesAllButPDF(t *testing.T) { app.consts.Store(&constants{UploadProvider: "fs"}) tests := []struct { + name string contentType string + download bool wantType string wantDisposition string wantSandbox bool }{ - {"image/svg+xml", "image/svg+xml", "attachment", true}, - {"image/svg+xml; x", "image/svg+xml", "attachment", true}, - {"image/x+xml", "image/x+xml", "attachment", true}, - {"text/plain,image/svg+xml", "application/octet-stream", "attachment", true}, - {"image/png", "image/png", "inline", true}, - {"application/pdf", "application/pdf", "inline", false}, + {"svg", "image/svg+xml", false, "image/svg+xml", "attachment", true}, + {"svg with bad param", "image/svg+xml; x", false, "image/svg+xml", "attachment", true}, + {"other xml image", "image/x+xml", false, "image/x+xml", "attachment", true}, + {"unparseable", "text/plain,image/svg+xml", false, "application/octet-stream", "attachment", true}, + {"png", "image/png", false, "image/png", "inline", true}, + {"png forced download", "image/png", true, "image/png", "attachment", true}, + {"pdf", "application/pdf", false, "application/pdf", "inline", false}, + {"pdf forced download", "application/pdf", true, "application/pdf", "attachment", false}, } for _, tt := range tests { - t.Run(tt.contentType, func(t *testing.T) { + t.Run(tt.name, func(t *testing.T) { req := &fastglue.Request{RequestCtx: &fasthttp.RequestCtx{}, Context: app} + if tt.download { + req.RequestCtx.Request.SetRequestURI("/uploads/" + uuid + "?download=1") + } media := &mmodels.Media{ContentType: tt.contentType, Filename: "file", Private: true} if err := serveMediaFile(req, app, uuid, media); err != nil { t.Fatal(err) diff --git a/internal/media/media.go b/internal/media/media.go index 17c68229..999d538f 100644 --- a/internal/media/media.go +++ b/internal/media/media.go @@ -290,7 +290,7 @@ func (m *Manager) GetThumbnailURL(uuid string) string { return u.String() } } - return m.store.GetURL(image.ThumbPrefix+uuid, models.DispositionInline, "") + return m.store.GetURL(image.ThumbPrefix+uuid, models.DispositionInline, "" /** fileName **/) } // SignedURLValidator returns the store's signature validator if available. From 6f77cfb1ad2783d2cdb51c65a5860f714988e3dc Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Fri, 25 Sep 2026 20:01:43 +0530 Subject: [PATCH 4/4] sandbox only downloaded files so videos play in Chrome again --- cmd/media.go | 5 +++-- cmd/media_test.go | 9 ++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/cmd/media.go b/cmd/media.go index 8a0e44a6..b539d8c4 100644 --- a/cmd/media.go +++ b/cmd/media.go @@ -265,8 +265,9 @@ func serveMediaFile(r *fastglue.Request, app *App, uuid string, media *mmodels.M r.RequestCtx.Response.Header.Set("Content-Type", contentType) r.RequestCtx.Response.Header.Set("Content-Disposition", mime.FormatMediaType(disposition, map[string]string{"filename": media.Filename})) r.RequestCtx.Response.Header.Set("X-Content-Type-Options", "nosniff") - // Chrome's PDF viewer does not load in a sandboxed page. - if contentType != mmodels.ContentTypePDF { + // Chrome's video and PDF viewers break in a sandbox, and inline types run no script anyway. + // Downloads are sandboxed for webviews that render attachments inline. + if disposition == mmodels.DispositionAttachment { r.RequestCtx.Response.Header.Set("Content-Security-Policy", "sandbox") } r.RequestCtx.Response.Header.Set("Cache-Control", fmt.Sprintf("%s, max-age=%d, immutable", cacheVisibility(media.Private), int(mediaCacheTTL.Seconds()))) diff --git a/cmd/media_test.go b/cmd/media_test.go index f6330454..b8234bff 100644 --- a/cmd/media_test.go +++ b/cmd/media_test.go @@ -39,7 +39,7 @@ func TestPrepareImageUploadAllowsOversizedImageWithoutThumbnail(t *testing.T) { } } -func TestServeMediaFileSandboxesAllButPDF(t *testing.T) { +func TestServeMediaFileSandboxesDownloads(t *testing.T) { const uuid = "0b7a3c1e-2f4d-4b8a-9c6e-1d2f3a4b5c6d" dir := t.TempDir() svg := []byte(``) @@ -66,10 +66,13 @@ func TestServeMediaFileSandboxesAllButPDF(t *testing.T) { {"svg with bad param", "image/svg+xml; x", false, "image/svg+xml", "attachment", true}, {"other xml image", "image/x+xml", false, "image/x+xml", "attachment", true}, {"unparseable", "text/plain,image/svg+xml", false, "application/octet-stream", "attachment", true}, - {"png", "image/png", false, "image/png", "inline", true}, + {"html", "text/html", false, "text/html", "attachment", true}, + {"png", "image/png", false, "image/png", "inline", false}, {"png forced download", "image/png", true, "image/png", "attachment", true}, + {"video", "video/mp4", false, "video/mp4", "inline", false}, + {"video forced download", "video/mp4", true, "video/mp4", "attachment", true}, {"pdf", "application/pdf", false, "application/pdf", "inline", false}, - {"pdf forced download", "application/pdf", true, "application/pdf", "attachment", false}, + {"pdf forced download", "application/pdf", true, "application/pdf", "attachment", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {