From c27b7bab5eceade3657d579fe0219f024aa40c6d Mon Sep 17 00:00:00 2001 From: yushen Date: Tue, 31 Mar 2026 14:58:52 +0800 Subject: [PATCH] fix(upload): sniff content type, sanitize filename, add key prefix - Use http.DetectContentType() instead of trusting client-declared MIME type - Sanitize quotes in filename for Content-Disposition header injection - Add uploads/ prefix to S3 keys for better organization Co-Authored-By: Claude Opus 4.6 (1M context) --- server/internal/handler/file.go | 16 ++++++++++++++-- server/internal/storage/s3.go | 3 ++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/server/internal/handler/file.go b/server/internal/handler/file.go index ab7882ee..096e9a13 100644 --- a/server/internal/handler/file.go +++ b/server/internal/handler/file.go @@ -59,11 +59,23 @@ func (h *Handler) UploadFile(w http.ResponseWriter, r *http.Request) { } defer file.Close() - contentType := header.Header.Get("Content-Type") + // Sniff actual content type from file bytes instead of trusting the client header. + buf := make([]byte, 512) + n, err := file.Read(buf) + if err != nil && err != io.EOF { + writeError(w, http.StatusBadRequest, "failed to read file") + return + } + contentType := http.DetectContentType(buf[:n]) if !isContentTypeAllowed(contentType) { writeError(w, http.StatusBadRequest, fmt.Sprintf("file type not allowed: %s", contentType)) return } + // Seek back so the full file is uploaded. + if _, err := file.Seek(0, io.SeekStart); err != nil { + writeError(w, http.StatusInternalServerError, "failed to read file") + return + } data, err := io.ReadAll(file) if err != nil { @@ -77,7 +89,7 @@ func (h *Handler) UploadFile(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusInternalServerError, "internal error") return } - key := hex.EncodeToString(b) + path.Ext(header.Filename) + key := "uploads/" + hex.EncodeToString(b) + path.Ext(header.Filename) link, err := h.Storage.Upload(r.Context(), key, data, contentType, header.Filename) if err != nil { diff --git a/server/internal/storage/s3.go b/server/internal/storage/s3.go index 3f03ea86..b29375f0 100644 --- a/server/internal/storage/s3.go +++ b/server/internal/storage/s3.go @@ -6,6 +6,7 @@ import ( "fmt" "log/slog" "os" + "strings" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" @@ -73,7 +74,7 @@ func (s *S3Storage) Upload(ctx context.Context, key string, data []byte, content Key: aws.String(key), Body: bytes.NewReader(data), ContentType: aws.String(contentType), - ContentDisposition: aws.String(fmt.Sprintf(`inline; filename="%s"`, filename)), + ContentDisposition: aws.String(fmt.Sprintf(`inline; filename="%s"`, strings.ReplaceAll(filename, `"`, "'"))), CacheControl: aws.String("max-age=432000,public"), StorageClass: types.StorageClassIntelligentTiering, })