157 lines
4.3 KiB
Go
157 lines
4.3 KiB
Go
package storage
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"io"
|
|
"mime"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
const slugChars = "23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"
|
|
|
|
var safeFilenameRegex = regexp.MustCompile(`[^a-zA-Z0-9._-]+`)
|
|
|
|
func GenerateSlug(length int) string {
|
|
b := make([]byte, length)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "x" + strings.ToLower(slugChars[:length-1])
|
|
}
|
|
charsLen := byte(len(slugChars))
|
|
for i := 0; i < length; i++ {
|
|
b[i] = slugChars[b[i]%charsLen]
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func SanitizeFilename(name string) string {
|
|
// Normalizar separadores de ruta (incluyendo Windows \)
|
|
normalized := strings.ReplaceAll(name, "\\", "/")
|
|
base := filepath.Base(normalized)
|
|
|
|
// Eliminar secuencias '..' para evitar cualquier ambigüedad
|
|
for strings.Contains(base, "..") {
|
|
base = strings.ReplaceAll(base, "..", ".")
|
|
}
|
|
|
|
sanitized := safeFilenameRegex.ReplaceAllString(base, "_")
|
|
// Evitar archivos ocultos o nombres que empiecen por punto
|
|
sanitized = strings.TrimLeft(sanitized, "._-")
|
|
|
|
if len(sanitized) > 100 {
|
|
ext := filepath.Ext(sanitized)
|
|
sanitized = sanitized[:100-len(ext)] + ext
|
|
}
|
|
if sanitized == "" || sanitized == "." {
|
|
sanitized = "archivo.bin"
|
|
}
|
|
return sanitized
|
|
}
|
|
|
|
func DetectMimeType(filename string, reader io.ReadSeeker) string {
|
|
ext := strings.ToLower(filepath.Ext(filename))
|
|
|
|
// 1. Extensiones peligrosas de scripts, código y plantillas: NUNCA ejecutables como text/html o php
|
|
switch ext {
|
|
case ".html", ".htm", ".shtml", ".xhtml", ".hta":
|
|
return "text/plain; charset=utf-8"
|
|
case ".php", ".phtml", ".php3", ".php4", ".php5", ".pht", ".phar", ".inc", ".cgi", ".pl", ".py", ".sh", ".bash", ".sql":
|
|
return "text/plain; charset=utf-8"
|
|
case ".js", ".mjs", ".ts", ".jsx", ".tsx":
|
|
return "text/plain; charset=utf-8"
|
|
case ".txt", ".log", ".env", ".ini", ".conf", ".yaml", ".yml", ".md", ".go", ".rs", ".java", ".c", ".cpp", ".h", ".css":
|
|
return "text/plain; charset=utf-8"
|
|
case ".json":
|
|
return "application/json"
|
|
case ".pdf":
|
|
return "application/pdf"
|
|
case ".mp4":
|
|
return "video/mp4"
|
|
case ".webm":
|
|
return "video/webm"
|
|
case ".mp3":
|
|
return "audio/mpeg"
|
|
case ".wav":
|
|
return "audio/wav"
|
|
case ".ogg":
|
|
return "audio/ogg"
|
|
case ".png":
|
|
return "image/png"
|
|
case ".jpg", ".jpeg":
|
|
return "image/jpeg"
|
|
case ".gif":
|
|
return "image/gif"
|
|
case ".webp":
|
|
return "image/webp"
|
|
case ".svg":
|
|
return "image/svg+xml"
|
|
}
|
|
|
|
// 2. Consulta MIME del sistema para otras extensiones
|
|
if m := mime.TypeByExtension(ext); m != "" {
|
|
// Doble verificación: neutralizar si el sistema operativo clasifica como HTML o PHP
|
|
if strings.HasPrefix(m, "text/html") || strings.Contains(m, "php") || strings.Contains(m, "javascript") {
|
|
return "text/plain; charset=utf-8"
|
|
}
|
|
return m
|
|
}
|
|
|
|
// 3. Sniff de primeros 512 bytes
|
|
if reader != nil {
|
|
buf := make([]byte, 512)
|
|
n, err := reader.Read(buf)
|
|
_, _ = reader.Seek(0, io.SeekStart)
|
|
if err == nil && n > 0 {
|
|
detected := http.DetectContentType(buf[:n])
|
|
if detected != "application/octet-stream" {
|
|
if strings.HasPrefix(detected, "text/html") {
|
|
return "text/plain; charset=utf-8"
|
|
}
|
|
return detected
|
|
}
|
|
}
|
|
}
|
|
|
|
return "application/octet-stream"
|
|
}
|
|
|
|
func SaveFile(uploadDir, slug, originalFilename string, src io.Reader) (string, int64, error) {
|
|
if err := os.MkdirAll(uploadDir, 0750); err != nil {
|
|
return "", 0, err
|
|
}
|
|
|
|
// Crear archivo preventivo .htaccess para bloquear cualquier motor de script si Apache apuntara aquí
|
|
htaccessPath := filepath.Join(uploadDir, ".htaccess")
|
|
if _, err := os.Stat(htaccessPath); os.IsNotExist(err) {
|
|
htaccessContent := `# Seguridad drop.sigmac.app: Desactivar ejecución de scripts
|
|
Options -ExecCGI -Indexes
|
|
<FilesMatch "\.(php|phtml|phar|pl|py|cgi|sh|bash)$">
|
|
Require all denied
|
|
</FilesMatch>
|
|
`
|
|
_ = os.WriteFile(htaccessPath, []byte(htaccessContent), 0644)
|
|
}
|
|
|
|
safeName := SanitizeFilename(originalFilename)
|
|
storedFilename := slug + "_" + safeName
|
|
destPath := filepath.Join(uploadDir, storedFilename)
|
|
|
|
// Crear con permisos 0640 (sin flag +x ejecutable)
|
|
dest, err := os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0640)
|
|
if err != nil {
|
|
return "", 0, err
|
|
}
|
|
defer dest.Close()
|
|
|
|
written, err := io.Copy(dest, src)
|
|
if err != nil {
|
|
_ = os.Remove(destPath)
|
|
return "", 0, err
|
|
}
|
|
|
|
return storedFilename, written, nil
|
|
}
|