From 481695c93035a643c460b75f05f4c451fae4e6b8 Mon Sep 17 00:00:00 2001 From: csdbianhua Date: Wed, 18 Mar 2026 11:55:52 +0800 Subject: [PATCH] fix(execd): encode non-ASCII filenames in Content-Disposition header Use RFC 6266 and RFC 5987 encoding for filenames containing non-ASCII characters (e.g., Chinese, Japanese) in file download responses. - Add formatContentDisposition function to properly encode filenames - Add unit tests for ASCII and non-ASCII filename scenarios --- .../pkg/web/controller/filesystem_download.go | 25 ++++++++++- .../pkg/web/controller/filesystem_test.go | 41 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/components/execd/pkg/web/controller/filesystem_download.go b/components/execd/pkg/web/controller/filesystem_download.go index 0ab62a004..126688619 100644 --- a/components/execd/pkg/web/controller/filesystem_download.go +++ b/components/execd/pkg/web/controller/filesystem_download.go @@ -18,6 +18,7 @@ import ( "fmt" "io" "net/http" + "net/url" "os" "path/filepath" "strconv" @@ -55,7 +56,7 @@ func (c *FilesystemController) DownloadFile() { } c.ctx.Header("Content-Type", "application/octet-stream") - c.ctx.Header("Content-Disposition", "attachment; filename="+filepath.Base(filePath)) + c.ctx.Header("Content-Disposition", formatContentDisposition(filepath.Base(filePath))) c.ctx.Header("Content-Length", strconv.FormatInt(fileInfo.Size(), 10)) if rangeHeader := c.ctx.GetHeader("Range"); rangeHeader != "" { @@ -81,3 +82,25 @@ func (c *FilesystemController) DownloadFile() { http.ServeContent(c.ctx.Writer, c.ctx.Request, filepath.Base(filePath), fileInfo.ModTime(), file) } + +// formatContentDisposition formats the Content-Disposition header value with proper +// encoding for non-ASCII filenames according to RFC 6266 and RFC 5987. +func formatContentDisposition(filename string) string { + // Check if filename contains non-ASCII characters + needsEncoding := false + for _, r := range filename { + if r > 127 { + needsEncoding = true + break + } + } + + if !needsEncoding { + return "attachment; filename=\"" + filename + "\"" + } + + // Use RFC 5987 encoding for non-ASCII filenames + // Format: attachment; filename="fallback"; filename*=UTF-8''encoded_name + encodedFilename := url.PathEscape(filename) + return "attachment; filename=\"" + encodedFilename + "\"; filename*=UTF-8''" + encodedFilename +} diff --git a/components/execd/pkg/web/controller/filesystem_test.go b/components/execd/pkg/web/controller/filesystem_test.go index 804e60df8..c340cc45a 100644 --- a/components/execd/pkg/web/controller/filesystem_test.go +++ b/components/execd/pkg/web/controller/filesystem_test.go @@ -118,3 +118,44 @@ func TestReplaceContentFailsUnknownFile(t *testing.T) { require.Contains(t, []int{http.StatusNotFound, http.StatusInternalServerError}, rec.Code, "expected failure status") } + +func TestFormatContentDisposition(t *testing.T) { + tests := []struct { + name string + filename string + want string + }{ + { + name: "ASCII filename", + filename: "test.txt", + want: "attachment; filename=\"test.txt\"", + }, + { + name: "Chinese filename", + filename: "测试文件.txt", + want: "attachment; filename=\"%E6%B5%8B%E8%AF%95%E6%96%87%E4%BB%B6.txt\"; filename*=UTF-8''%E6%B5%8B%E8%AF%95%E6%96%87%E4%BB%B6.txt", + }, + { + name: "Japanese filename", + filename: "テスト.txt", + want: "attachment; filename=\"%E3%83%86%E3%82%B9%E3%83%88.txt\"; filename*=UTF-8''%E3%83%86%E3%82%B9%E3%83%88.txt", + }, + { + name: "Special characters in filename", + filename: "file with spaces.txt", + want: "attachment; filename=\"file with spaces.txt\"", + }, + { + name: "Mixed ASCII and non-ASCII", + filename: "report-报告.pdf", + want: "attachment; filename=\"report-%E6%8A%A5%E5%91%8A.pdf\"; filename*=UTF-8''report-%E6%8A%A5%E5%91%8A.pdf", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := formatContentDisposition(tt.filename) + require.Equal(t, tt.want, got) + }) + } +}