security: blindaje contra ejecucion de scripts RCE, Stored XSS, MIME sniffing, Path Traversal y DoS
This commit is contained in:
+65
-30
@@ -28,8 +28,19 @@ func GenerateSlug(length int) string {
|
||||
}
|
||||
|
||||
func SanitizeFilename(name string) string {
|
||||
base := filepath.Base(name)
|
||||
// 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
|
||||
@@ -42,25 +53,19 @@ func SanitizeFilename(name string) string {
|
||||
|
||||
func DetectMimeType(filename string, reader io.ReadSeeker) string {
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
if m := mime.TypeByExtension(ext); m != "" {
|
||||
return m
|
||||
}
|
||||
|
||||
// Sniff first 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" {
|
||||
return detected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Known fallback extensions
|
||||
// 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":
|
||||
@@ -83,29 +88,59 @@ func DetectMimeType(filename string, reader io.ReadSeeker) string {
|
||||
return "image/webp"
|
||||
case ".svg":
|
||||
return "image/svg+xml"
|
||||
case ".txt", ".log", ".env":
|
||||
return "text/plain; charset=utf-8"
|
||||
case ".json":
|
||||
return "application/json"
|
||||
case ".js":
|
||||
return "application/javascript"
|
||||
case ".go", ".py", ".rs", ".java", ".c", ".cpp", ".sh", ".sql", ".html", ".css", ".md":
|
||||
return "text/plain; charset=utf-8"
|
||||
default:
|
||||
return "application/octet-stream"
|
||||
}
|
||||
|
||||
// 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, 0755); err != nil {
|
||||
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)
|
||||
|
||||
dest, err := os.Create(destPath)
|
||||
// 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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user