security: blindaje contra ejecucion de scripts RCE, Stored XSS, MIME sniffing, Path Traversal y DoS
This commit is contained in:
@@ -211,12 +211,15 @@ func (h *Handlers) ShareText(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Límite de 5 MB para evitar DoS por memoria
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 5<<20)
|
||||
|
||||
var req struct {
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || strings.TrimSpace(req.Text) == "" {
|
||||
http.Error(w, "Contenido de texto requerido", http.StatusBadRequest)
|
||||
http.Error(w, "Contenido de texto requerido o tamaño excesivo (máx 5 MB)", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -322,6 +325,17 @@ func (h *Handlers) View(w http.ResponseWriter, r *http.Request) {
|
||||
_ = h.db.IncrementViews(id)
|
||||
share.ViewsCount++
|
||||
|
||||
// Si es un archivo de texto/código y no tiene TextContent, leer hasta 512 KB para vista previa segura
|
||||
isText := strings.HasPrefix(share.MimeType, "text/") || share.Type == "text"
|
||||
if isText && share.TextContent == "" && share.StoredPath != "" {
|
||||
fullPath := filepath.Join(h.cfg.UploadDir, share.StoredPath)
|
||||
if fi, err := os.Stat(fullPath); err == nil && fi.Size() <= 512*1024 {
|
||||
if content, err := os.ReadFile(fullPath); err == nil {
|
||||
share.TextContent = string(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
session := h.auth.GetOrCreateSession(w, r)
|
||||
data := PageData{
|
||||
Session: session,
|
||||
@@ -331,7 +345,7 @@ func (h *Handlers) View(w http.ResponseWriter, r *http.Request) {
|
||||
IsVideo: strings.HasPrefix(share.MimeType, "video/"),
|
||||
IsAudio: strings.HasPrefix(share.MimeType, "audio/"),
|
||||
IsPDF: share.MimeType == "application/pdf",
|
||||
IsText: strings.HasPrefix(share.MimeType, "text/") || share.Type == "text",
|
||||
IsText: isText,
|
||||
}
|
||||
|
||||
tmpl := h.templates["viewer.html"]
|
||||
@@ -351,6 +365,35 @@ func (h *Handlers) renderExpired(w http.ResponseWriter, r *http.Request) {
|
||||
_ = tmpl.ExecuteTemplate(w, "base.html", data)
|
||||
}
|
||||
|
||||
func isSafeInlineMime(mimeType string) bool {
|
||||
switch mimeType {
|
||||
case "image/png", "image/jpeg", "image/gif", "image/webp", "image/bmp", "image/x-icon":
|
||||
return true
|
||||
case "audio/mpeg", "audio/wav", "audio/ogg", "audio/webm", "audio/aac", "audio/flac":
|
||||
return true
|
||||
case "video/mp4", "video/webm", "video/ogg":
|
||||
return true
|
||||
case "application/pdf":
|
||||
return true
|
||||
case "text/plain", "text/plain; charset=utf-8":
|
||||
return true
|
||||
case "image/svg+xml":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func sanitizeHeaderFilename(name string) string {
|
||||
cleaned := strings.ReplaceAll(name, "\r", "")
|
||||
cleaned = strings.ReplaceAll(cleaned, "\n", "")
|
||||
cleaned = strings.ReplaceAll(cleaned, "\"", "_")
|
||||
if strings.TrimSpace(cleaned) == "" {
|
||||
cleaned = "archivo.bin"
|
||||
}
|
||||
return cleaned
|
||||
}
|
||||
|
||||
func (h *Handlers) Download(w http.ResponseWriter, r *http.Request) {
|
||||
id := strings.TrimPrefix(r.URL.Path, "/d/")
|
||||
if id == "" {
|
||||
@@ -371,9 +414,12 @@ func (h *Handlers) Download(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
_ = h.db.IncrementDownloads(id)
|
||||
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
safeTitle := sanitizeHeaderFilename(share.Title)
|
||||
|
||||
if share.Type == "text" {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", share.Title+".txt"))
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", safeTitle+".txt"))
|
||||
_, _ = w.Write([]byte(share.TextContent))
|
||||
return
|
||||
}
|
||||
@@ -384,7 +430,7 @@ func (h *Handlers) Download(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", share.Title))
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", safeTitle))
|
||||
http.ServeFile(w, r, fullPath)
|
||||
}
|
||||
|
||||
@@ -406,8 +452,14 @@ func (h *Handlers) Raw(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Cabeceras de seguridad estrictas contra XSS y MIME-sniffing
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
|
||||
|
||||
if share.Type == "text" {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Header().Set("Content-Disposition", "inline")
|
||||
_, _ = w.Write([]byte(share.TextContent))
|
||||
return
|
||||
}
|
||||
@@ -418,8 +470,21 @@ func (h *Handlers) Raw(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", share.MimeType)
|
||||
w.Header().Set("Content-Disposition", "inline")
|
||||
// Neutralizar cualquier intento de servir HTML, PHP o scripts ejecutables inline
|
||||
mimeType := share.MimeType
|
||||
if strings.HasPrefix(mimeType, "text/html") || strings.Contains(mimeType, "php") || strings.Contains(mimeType, "javascript") {
|
||||
mimeType = "text/plain; charset=utf-8"
|
||||
}
|
||||
w.Header().Set("Content-Type", mimeType)
|
||||
|
||||
if isSafeInlineMime(mimeType) {
|
||||
w.Header().Set("Content-Disposition", "inline")
|
||||
} else {
|
||||
// Archivos ejecutables, binarios o desconocidos se fuerzan a descarga
|
||||
safeTitle := sanitizeHeaderFilename(share.Title)
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", safeTitle))
|
||||
}
|
||||
|
||||
http.ServeFile(w, r, fullPath)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user