fix(upload): add file type/size validation, Content-Disposition header
- Add content type allowlist (images, PDF, text, video, audio, zip) - Enforce 10 MB upload limit via http.MaxBytesReader - Set Content-Disposition on S3 objects for proper download filenames - Remove unused CloudFrontSigner.Domain() method Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
29a80e057e
commit
edf4c00c08
4 changed files with 54 additions and 26 deletions
|
|
@ -8,16 +8,46 @@ import (
|
|||
"log/slog"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const maxUploadSize = 10 << 20 // 10 MB
|
||||
|
||||
// Allowed MIME type prefixes and exact types for uploads.
|
||||
var allowedContentTypes = map[string]bool{
|
||||
"image/png": true,
|
||||
"image/jpeg": true,
|
||||
"image/gif": true,
|
||||
"image/webp": true,
|
||||
"image/svg+xml": true,
|
||||
"application/pdf": true,
|
||||
"text/plain": true,
|
||||
"text/csv": true,
|
||||
"application/json": true,
|
||||
"video/mp4": true,
|
||||
"video/webm": true,
|
||||
"audio/mpeg": true,
|
||||
"audio/wav": true,
|
||||
"application/zip": true,
|
||||
}
|
||||
|
||||
func isContentTypeAllowed(ct string) bool {
|
||||
// Normalize: take only the media type, strip parameters like charset.
|
||||
ct = strings.TrimSpace(strings.SplitN(ct, ";", 2)[0])
|
||||
ct = strings.ToLower(ct)
|
||||
return allowedContentTypes[ct]
|
||||
}
|
||||
|
||||
func (h *Handler) UploadFile(w http.ResponseWriter, r *http.Request) {
|
||||
if h.Storage == nil {
|
||||
writeError(w, http.StatusServiceUnavailable, "file upload not configured")
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseMultipartForm(32 << 20); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid multipart form")
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)
|
||||
|
||||
if err := r.ParseMultipartForm(maxUploadSize); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "file too large or invalid multipart form")
|
||||
return
|
||||
}
|
||||
defer r.MultipartForm.RemoveAll()
|
||||
|
|
@ -29,9 +59,15 @@ func (h *Handler) UploadFile(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
defer file.Close()
|
||||
|
||||
contentType := header.Header.Get("Content-Type")
|
||||
if !isContentTypeAllowed(contentType) {
|
||||
writeError(w, http.StatusBadRequest, fmt.Sprintf("file type not allowed: %s", contentType))
|
||||
return
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, fmt.Sprintf("failed to read file: %v", err))
|
||||
writeError(w, http.StatusBadRequest, "failed to read file")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -43,10 +79,7 @@ func (h *Handler) UploadFile(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
key := hex.EncodeToString(b) + path.Ext(header.Filename)
|
||||
|
||||
contentType := header.Header.Get("Content-Type")
|
||||
link, err := h.Storage.Upload(r.Context(), key, data, contentType, map[string]string{
|
||||
"filename": header.Filename,
|
||||
})
|
||||
link, err := h.Storage.Upload(r.Context(), key, data, contentType, header.Filename)
|
||||
if err != nil {
|
||||
slog.Error("file upload failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "upload failed")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue