security: remediate all P0-P2 audit findings (12 tasks)
P0 fixes: - WebSocket origin checking (reject untrusted origins) - WS session token moved from URL query param to first message frame - WebAuthn login cookie now uses Secure flag via shared SetSessionCookie - PostgreSQL sslmode configurable via POSTGRES_SSLMODE env (default: require) - Fixed DatabaseDSN to use real password instead of masked placeholder P1 fixes: - Per-IP rate limiting middleware (auth: 5 req/s, invites: 2 req/s) - Security headers on all responses (CSP, X-Frame-Options, nosniff, etc.) - WS broadcasts scoped to server members (prevents cross-server data leak) - Webhook tokens stored as SHA-256 hashes (not plaintext) P2 fixes: - CSRF protection via Origin header validation on state-changing requests - MANAGE_CHANNELS permission enforced on channel update/delete - Upload validation: 25MB limit, extension allowlist, server-side MIME check - File serve: path traversal protection + Content-Disposition: attachment Files: 23 changed, +499/-100. Builds clean (go build, go vet, npm build). Zero CVEs (govulncheck, npm audit).
This commit is contained in:
@@ -14,6 +14,16 @@ import (
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
)
|
||||
|
||||
const maxUploadSize = 25 << 20 // 25 MB per file
|
||||
|
||||
var allowedExtensions = map[string]bool{
|
||||
".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true,
|
||||
".mp3": true, ".wav": true, ".ogg": true, ".flac": true,
|
||||
".mp4": true, ".webm": true, ".mov": true,
|
||||
".pdf": true, ".txt": true, ".md": true, ".json": true, ".csv": true,
|
||||
".zip": true, ".tar": true, ".gz": true,
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
client *minio.Client
|
||||
bucket string
|
||||
@@ -53,8 +63,10 @@ func NewHandler(cfg *config.Config) (*Handler, error) {
|
||||
}
|
||||
|
||||
func (h *Handler) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseMultipartForm(50 << 20); err != nil {
|
||||
http.Error(w, `{"error":"invalid form"}`, http.StatusBadRequest)
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)
|
||||
|
||||
if err := r.ParseMultipartForm(maxUploadSize); err != nil {
|
||||
http.Error(w, `{"error":"file too large or invalid form"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -65,14 +77,33 @@ func (h *Handler) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
contentType := header.Header.Get("Content-Type")
|
||||
if contentType == "" {
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
|
||||
// Validate extension against allowlist
|
||||
ext := ""
|
||||
if idx := strings.LastIndex(header.Filename, "."); idx >= 0 {
|
||||
ext = header.Filename[idx:]
|
||||
ext = strings.ToLower(header.Filename[idx:])
|
||||
}
|
||||
if !allowedExtensions[ext] {
|
||||
http.Error(w, `{"error":"file type not allowed"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Server-side content-type detection (blocks HTML/JS that could be used for XSS)
|
||||
buf := make([]byte, 512)
|
||||
n, _ := file.Read(buf)
|
||||
detectedType := http.DetectContentType(buf[:n])
|
||||
file.Seek(0, io.SeekStart)
|
||||
|
||||
blockedTypes := []string{"text/html", "application/javascript", "application/x-executable"}
|
||||
for _, blocked := range blockedTypes {
|
||||
if strings.HasPrefix(detectedType, blocked) {
|
||||
http.Error(w, `{"error":"file type not allowed"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
contentType := header.Header.Get("Content-Type")
|
||||
if contentType == "" {
|
||||
contentType = detectedType
|
||||
}
|
||||
|
||||
objectName := uuid.New().String() + ext
|
||||
@@ -98,6 +129,12 @@ func (h *Handler) Serve(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Sanitize: reject path traversal attempts
|
||||
if strings.Contains(objectName, "..") || strings.Contains(objectName, "/") {
|
||||
http.Error(w, `{"error":"invalid path"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -116,5 +153,6 @@ func (h *Handler) Serve(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("Content-Type", stat.ContentType)
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size))
|
||||
w.Header().Set("Content-Disposition", "attachment")
|
||||
io.Copy(w, obj)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user