veza/veza-backend-api/internal/utils/utils.go
senke 9cd0da0046 fix(v0.12.6): apply all pentest remediations — 36 findings across 36 files
CRITICAL fixes:
- Race condition (TOCTOU) in payout/refund with SELECT FOR UPDATE (CRITICAL-001/002)
- IDOR on analytics endpoint — ownership check enforced (CRITICAL-003)
- CSWSH on all WebSocket endpoints — origin whitelist (CRITICAL-004)
- Mass assignment on user self-update — strip privileged fields (CRITICAL-005)

HIGH fixes:
- Path traversal in marketplace upload — UUID filenames (HIGH-001)
- IP spoofing — use Gin trusted proxy c.ClientIP() (HIGH-002)
- Popularity metrics (followers, likes) set to json:"-" (HIGH-003)
- bcrypt cost hardened to 12 everywhere (HIGH-004)
- Refresh token lock made mandatory (HIGH-005)
- Stream token replay prevention with access_count (HIGH-006)
- Subscription trial race condition fixed (HIGH-007)
- License download expiration check (HIGH-008)
- Webhook amount validation (HIGH-009)
- pprof endpoint removed from production (HIGH-010)

MEDIUM fixes:
- WebSocket message size limit 64KB (MEDIUM-010)
- HSTS header in nginx production (MEDIUM-001)
- CORS origin restricted in nginx-rtmp (MEDIUM-002)
- Docker alpine pinned to 3.21 (MEDIUM-003/004)
- Redis authentication enforced (MEDIUM-005)
- GDPR account deletion expanded (MEDIUM-006)
- .gitignore hardened (MEDIUM-007)

LOW/INFO fixes:
- GitHub Actions SHA pinning on all workflows (LOW-001)
- .env.example security documentation (INFO-001)
- Production CORS set to HTTPS (LOW-002)

All tests pass. Go and Rust compile clean.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 00:44:46 +01:00

378 lines
9.1 KiB
Go

package utils
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"math/big"
"regexp"
"strings"
"time"
"unicode"
"golang.org/x/crypto/bcrypt"
)
// GenerateID génère un ID unique
func GenerateID() string {
b := make([]byte, 16)
rand.Read(b)
return hex.EncodeToString(b)
}
// GenerateUUID génère un UUID v4
func GenerateUUID() string {
b := make([]byte, 16)
rand.Read(b)
// Version 4
b[6] = (b[6] & 0x0f) | 0x40
// Variant bits
b[8] = (b[8] & 0x3f) | 0x80
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
}
// GenerateRandomString génère une chaîne aléatoire de longueur donnée
func GenerateRandomString(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, length)
for i := range b {
num, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
b[i] = charset[num.Int64()]
}
return string(b)
}
// GenerateRandomBytes génère des bytes aléatoires
func GenerateRandomBytes(length int) ([]byte, error) {
b := make([]byte, length)
_, err := rand.Read(b)
return b, err
}
// HashPassword hash un mot de passe avec bcrypt
func HashPassword(password string) (string, error) {
hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), 12) // SECURITY(REM-030): Aligned with password_service.go bcryptCost=12
if err != nil {
return "", fmt.Errorf("failed to hash password: %w", err)
}
return string(hashedBytes), nil
}
// VerifyPassword vérifie un mot de passe contre son hash
func VerifyPassword(hashedPassword, password string) error {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}
// CheckPasswordHash est un alias pour VerifyPassword (compatibilité)
func CheckPasswordHash(password, hashedPassword string) error {
return VerifyPassword(hashedPassword, password)
}
// HashSHA256 hash une chaîne avec SHA256
func HashSHA256(input string) string {
hash := sha256.Sum256([]byte(input))
return hex.EncodeToString(hash[:])
}
// ValidateEmail valide le format d'un email
func ValidateEmail(email string) bool {
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
return emailRegex.MatchString(email)
}
// ValidatePasswordStrength is now in password_validator.go
// T0197: Moved to password_validator.go for better organization
// ValidateUsername valide le format d'un nom d'utilisateur
func ValidateUsername(username string) (bool, []string) {
var errors []string
if len(username) < 3 {
errors = append(errors, "Username must be at least 3 characters long")
}
if len(username) > 30 {
errors = append(errors, "Username must be less than 30 characters")
}
usernameRegex := regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
if !usernameRegex.MatchString(username) {
errors = append(errors, "Username can only contain letters, numbers, underscores, and hyphens")
}
return len(errors) == 0, errors
}
// SanitizeString nettoie une chaîne de caractères
func SanitizeString(input string) string {
// Supprimer les caractères de contrôle
cleaned := strings.Map(func(r rune) rune {
if r < 32 || r == 127 {
return -1
}
return r
}, input)
// Supprimer les espaces en début et fin
cleaned = strings.TrimSpace(cleaned)
// Limiter la longueur
if len(cleaned) > 1000 {
cleaned = cleaned[:1000]
}
return cleaned
}
// SanitizeHTML is now in sanitizer.go with enhanced functionality
// BE-SEC-009: Moved to sanitizer.go for better organization and security
// TruncateString tronque une chaîne à la longueur spécifiée
func TruncateString(input string, maxLength int) string {
if len(input) <= maxLength {
return input
}
return input[:maxLength] + "..."
}
// ContainsString vérifie si une chaîne contient une sous-chaîne (insensible à la casse)
func ContainsString(s, substr string) bool {
return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
}
// IsEmpty vérifie si une chaîne est vide ou ne contient que des espaces
func IsEmpty(s string) bool {
return strings.TrimSpace(s) == ""
}
// IsNotEmpty vérifie si une chaîne n'est pas vide
func IsNotEmpty(s string) bool {
return !IsEmpty(s)
}
// FormatDuration formate une durée en chaîne lisible
func FormatDuration(d time.Duration) string {
if d < time.Minute {
return fmt.Sprintf("%.0fs", d.Seconds())
}
if d < time.Hour {
return fmt.Sprintf("%.0fm", d.Minutes())
}
if d < 24*time.Hour {
return fmt.Sprintf("%.1fh", d.Hours())
}
return fmt.Sprintf("%.1fd", d.Hours()/24)
}
// FormatFileSize formate une taille de fichier en chaîne lisible
func FormatFileSize(bytes int64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
}
// FormatNumber formate un nombre avec des séparateurs de milliers
func FormatNumber(n int64) string {
str := fmt.Sprintf("%d", n)
if len(str) <= 3 {
return str
}
var result strings.Builder
for i, char := range str {
if i > 0 && (len(str)-i)%3 == 0 {
result.WriteString(",")
}
result.WriteRune(char)
}
return result.String()
}
// ParseDuration parse une durée depuis une chaîne
func ParseDuration(s string) (time.Duration, error) {
// Supprimer les espaces
s = strings.TrimSpace(s)
// Ajouter 's' si pas d'unité spécifiée
if !strings.ContainsAny(s, "smhd") {
s += "s"
}
return time.ParseDuration(s)
}
// IsValidURL vérifie si une chaîne est une URL valide
func IsValidURL(url string) bool {
urlRegex := regexp.MustCompile(`^https?://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(/.*)?$`)
return urlRegex.MatchString(url)
}
// ExtractDomain extrait le domaine d'une URL
func ExtractDomain(url string) string {
// Supprimer le protocole
if strings.HasPrefix(url, "http://") {
url = url[7:]
} else if strings.HasPrefix(url, "https://") {
url = url[8:]
}
// Supprimer le chemin
if idx := strings.Index(url, "/"); idx != -1 {
url = url[:idx]
}
return url
}
// GenerateSlug génère un slug à partir d'une chaîne
func GenerateSlug(input string) string {
// Convertir en minuscules
slug := strings.ToLower(input)
// Remplacer les espaces par des tirets
slug = strings.ReplaceAll(slug, " ", "-")
// Supprimer les caractères non alphanumériques sauf les tirets
slug = regexp.MustCompile(`[^a-z0-9-]`).ReplaceAllString(slug, "")
// Supprimer les tirets multiples
slug = regexp.MustCompile(`-+`).ReplaceAllString(slug, "-")
// Supprimer les tirets en début et fin
slug = strings.Trim(slug, "-")
return slug
}
// ContainsOnlyDigits vérifie si une chaîne ne contient que des chiffres
func ContainsOnlyDigits(s string) bool {
for _, char := range s {
if !unicode.IsDigit(char) {
return false
}
}
return true
}
// ContainsOnlyLetters vérifie si une chaîne ne contient que des lettres
func ContainsOnlyLetters(s string) bool {
for _, char := range s {
if !unicode.IsLetter(char) {
return false
}
}
return true
}
// ContainsOnlyAlphanumeric vérifie si une chaîne ne contient que des caractères alphanumériques
func ContainsOnlyAlphanumeric(s string) bool {
for _, char := range s {
if !unicode.IsLetter(char) && !unicode.IsDigit(char) {
return false
}
}
return true
}
// RemoveDuplicates supprime les doublons d'une slice de chaînes
func RemoveDuplicates(slice []string) []string {
keys := make(map[string]bool)
var result []string
for _, item := range slice {
if !keys[item] {
keys[item] = true
result = append(result, item)
}
}
return result
}
// Contains vérifie si une slice contient un élément
func Contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
// IndexOf retourne l'index d'un élément dans une slice
func IndexOf(slice []string, item string) int {
for i, s := range slice {
if s == item {
return i
}
}
return -1
}
// Reverse inverse l'ordre d'une slice
func Reverse(slice []string) []string {
for i, j := 0, len(slice)-1; i < j; i, j = i+1, j-1 {
slice[i], slice[j] = slice[j], slice[i]
}
return slice
}
// Chunk divise une slice en chunks de taille donnée
func Chunk(slice []string, chunkSize int) [][]string {
var chunks [][]string
for i := 0; i < len(slice); i += chunkSize {
end := i + chunkSize
if end > len(slice) {
end = len(slice)
}
chunks = append(chunks, slice[i:end])
}
return chunks
}
// Filter filtre une slice selon une condition
func Filter(slice []string, predicate func(string) bool) []string {
var result []string
for _, item := range slice {
if predicate(item) {
result = append(result, item)
}
}
return result
}
// Map applique une fonction à chaque élément d'une slice
func Map(slice []string, mapper func(string) string) []string {
var result []string
for _, item := range slice {
result = append(result, mapper(item))
}
return result
}
// Reduce réduit une slice à une seule valeur
func Reduce(slice []string, initial string, reducer func(string, string) string) string {
result := initial
for _, item := range slice {
result = reducer(result, item)
}
return result
}