# Security Best Practices & Hardening Guide ## Overview This document outlines security best practices for the Fotbal Club application based on OWASP Top 10 and industry standards. --- ## 1. Authentication & Authorization ### Current Implementation ✅ - JWT-based authentication with HttpOnly cookies - Password hashing using bcrypt - Role-based access control (admin, editor, user) ### Improvements Needed #### 1.1 Account Lockout **Risk**: Brute force attacks on login endpoint **Implementation**: ```go // internal/middleware/ratelimit.go - Add to auth controller type loginAttempts struct { sync.Mutex attempts map[string][]time.Time } var loginAttemptTracker = &loginAttempts{ attempts: make(map[string][]time.Time), } func (lat *loginAttempts) isLocked(email string) bool { lat.Lock() defer lat.Unlock() attempts, exists := lat.attempts[email] if !exists { return false } // Remove attempts older than 15 minutes cutoff := time.Now().Add(-15 * time.Minute) var recent []time.Time for _, t := range attempts { if t.After(cutoff) { recent = append(recent, t) } } lat.attempts[email] = recent // Lock after 5 failed attempts in 15 minutes return len(recent) >= 5 } func (lat *loginAttempts) recordFailure(email string) { lat.Lock() defer lat.Unlock() lat.attempts[email] = append(lat.attempts[email], time.Now()) } func (lat *loginAttempts) clearAttempts(email string) { lat.Lock() defer lat.Unlock() delete(lat.attempts, email) } ``` #### 1.2 Password Strength Requirements **Current**: No validation **Recommended**: Minimum 8 characters, mix of upper/lower/numbers/symbols ```go func ValidatePasswordStrength(password string) error { if len(password) < 8 { return errors.New("heslo musí mít alespoň 8 znaků") } var hasUpper, hasLower, hasNumber bool for _, char := range password { switch { case unicode.IsUpper(char): hasUpper = true case unicode.IsLower(char): hasLower = true case unicode.IsNumber(char): hasNumber = true } } if !hasUpper || !hasLower || !hasNumber { return errors.New("heslo musí obsahovat velká i malá písmena a čísla") } return nil } ``` #### 1.3 Session Management **Recommendation**: Implement session timeout and refresh tokens ```go // Add to JWT claims type Claims struct { UserID uint `json:"user_id"` Role string `json:"role"` Exp int64 `json:"exp"` Iat int64 `json:"iat"` Jti string `json:"jti"` // JWT ID for token revocation jwt.RegisteredClaims } // Token blacklist for logout var tokenBlacklist = sync.Map{} func RevokeToken(jti string) { tokenBlacklist.Store(jti, time.Now().Add(24 * time.Hour)) } func IsTokenRevoked(jti string) bool { _, exists := tokenBlacklist.Load(jti) return exists } ``` --- ## 2. Input Validation & Sanitization ### 2.1 Email Validation ```go func ValidateEmail(email string) bool { re := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) return re.MatchString(email) && len(email) <= 254 } ``` ### 2.2 URL Validation ```go func ValidateURL(rawURL string) error { if rawURL == "" { return nil } u, err := url.Parse(rawURL) if err != nil { return err } if u.Scheme != "http" && u.Scheme != "https" { return errors.New("pouze HTTP(S) URL jsou povoleny") } return nil } ``` ### 2.3 SQL Injection Prevention **Current**: ✅ Using GORM (parameterized queries) **Warning**: Avoid raw SQL unless necessary ```go // SAFE ✅ db.Where("email = ?", email).First(&user) // UNSAFE ❌ db.Raw("SELECT * FROM users WHERE email = '" + email + "'").Scan(&user) ``` --- ## 3. File Upload Security ### Current Issues - MIME type validation only - No antivirus scanning - No file size per user quota ### Improvements #### 3.1 Enhanced File Validation ```go func ValidateUploadedFile(file multipart.File, header *multipart.FileHeader) error { // Check file size if header.Size > 10*1024*1024 { // 10MB return errors.New("soubor je příliš velký") } // Read magic bytes buf := make([]byte, 512) _, err := file.Read(buf) if err != nil { return err } file.Seek(0, 0) // Reset // Validate MIME mime := http.DetectContentType(buf) allowed := []string{ "image/jpeg", "image/png", "image/gif", "image/webp", "application/pdf", } valid := false for _, m := range allowed { if m == mime { valid = true break } } if !valid { return errors.New("nepodporovaný typ souboru") } // Check extension matches MIME ext := strings.ToLower(filepath.Ext(header.Filename)) mimeToExt := map[string]string{ "image/jpeg": ".jpg", "image/png": ".png", "image/gif": ".gif", "image/webp": ".webp", "application/pdf": ".pdf", } expectedExt := mimeToExt[mime] if ext != expectedExt && ext != ".jpeg" { // Allow both .jpg and .jpeg return errors.New("přípona souboru neodpovídá typu") } return nil } ``` #### 3.2 User Upload Quota ```go func CheckUserUploadQuota(db *gorm.DB, userID uint) error { var totalSize int64 err := db.Model(&models.UploadedFile{}). Where("uploaded_by_id = ?", userID). Select("COALESCE(SUM(file_size), 0)"). Scan(&totalSize).Error if err != nil { return err } // 100MB quota per user if totalSize > 100*1024*1024 { return errors.New("překročena kvóta úložiště") } return nil } ``` --- ## 4. API Security ### 4.1 Rate Limiting by Endpoint ```go // More granular rate limits var rateLimits = map[string]struct{ max int; window time.Duration }{ "/api/v1/auth/login": {max: 5, window: 15 * time.Minute}, "/api/v1/contact": {max: 3, window: 1 * time.Hour}, "/api/v1/articles": {max: 100, window: 1 * time.Minute}, "/api/v1/upload": {max: 10, window: 1 * time.Hour}, } ``` ### 4.2 API Key Management (for external integrations) ```go type APIKey struct { gorm.Model Key string `gorm:"unique;not null"` Name string `gorm:"not null"` UserID uint `gorm:"not null"` ExpiresAt time.Time LastUsedAt *time.Time IsActive bool `gorm:"default:true"` } func ValidateAPIKey(key string) (*APIKey, error) { var apiKey APIKey err := db.Where("key = ? AND is_active = ? AND expires_at > ?", key, true, time.Now()).First(&apiKey).Error if err != nil { return nil, errors.New("neplatný API klíč") } // Update last used now := time.Now() apiKey.LastUsedAt = &now db.Save(&apiKey) return &apiKey, nil } ``` --- ## 5. Database Security ### 5.1 Connection Security ```go // Use SSL for database connection in production if config.AppConfig.AppEnv == "production" { dsn += "?sslmode=require" } ``` ### 5.2 Sensitive Data Encryption ```go // Encrypt sensitive fields at rest func EncryptSensitiveData(plaintext string) (string, error) { key := []byte(config.AppConfig.EncryptionKey) // 32 bytes block, err := aes.NewCipher(key) if err != nil { return "", err } gcm, err := cipher.NewGCM(block) if err != nil { return "", err } nonce := make([]byte, gcm.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return "", err } ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil) return base64.StdEncoding.EncodeToString(ciphertext), nil } ``` ### 5.3 Backup Encryption ```bash # Encrypt database backups pg_dump dbname | gpg -c > backup.sql.gpg ``` --- ## 6. Logging & Monitoring ### 6.1 Security Event Logging ```go func LogSecurityEvent(event string, userID uint, details map[string]interface{}) { entry := map[string]interface{}{ "timestamp": time.Now(), "event": event, "user_id": userID, "details": details, } logger.Warn("SECURITY: %+v", entry) // Store in database for audit trail auditLog := models.AuditLog{ Event: event, UserID: userID, Details: details, } db.Create(&auditLog) } // Usage LogSecurityEvent("login_failed", 0, map[string]interface{}{ "email": email, "ip": c.ClientIP(), }) ``` ### 6.2 Sensitive Data Redaction ```go func RedactSensitiveFields(data map[string]interface{}) map[string]interface{} { sensitive := []string{"password", "token", "secret", "api_key"} for _, field := range sensitive { if _, exists := data[field]; exists { data[field] = "***REDACTED***" } } return data } ``` --- ## 7. Environment Security ### 7.1 Environment Variable Validation ```go func ValidateRequiredEnvVars() error { required := []string{ "JWT_SECRET", "DATABASE_URL", "SMTP_HOST", } var missing []string for _, key := range required { if os.Getenv(key) == "" { missing = append(missing, key) } } if len(missing) > 0 { return fmt.Errorf("chybí povinné proměnné prostředí: %s", strings.Join(missing, ", ")) } // Validate JWT secret strength if len(os.Getenv("JWT_SECRET")) < 32 { return errors.New("JWT_SECRET musí mít alespoň 32 znaků") } return nil } ``` ### 7.2 Secrets Management ```bash # Use environment-specific .env files .env.development .env.staging .env.production # Never commit .env files # Use secret management tools in production: # - AWS Secrets Manager # - HashiCorp Vault # - Azure Key Vault ``` --- ## 8. Frontend Security ### 8.1 XSS Prevention ```typescript // Always sanitize user-generated content import DOMPurify from 'dompurify'; const SafeHTML = ({ html }: { html: string }) => { const clean = DOMPurify.sanitize(html, { ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a'], ALLOWED_ATTR: ['href'], }); return
; }; ``` ### 8.2 Sensitive Data in LocalStorage ```typescript // NEVER store sensitive data in localStorage // ❌ BAD localStorage.setItem('jwt_token', token); // ✅ GOOD - Use HttpOnly cookies // Token is set by backend in secure cookie ``` ### 8.3 Content Security Policy ```typescript // Report CSP violations window.addEventListener('securitypolicyviolation', (e) => { fetch('/api/v1/csp-report', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ violatedDirective: e.violatedDirective, blockedURI: e.blockedURI, documentURI: e.documentURI, }), }); }); ``` --- ## 9. Dependency Security ### 9.1 Regular Updates ```bash # Go dependencies go get -u ./... go mod tidy # npm dependencies npm audit npm audit fix # Check for known vulnerabilities npm install -g snyk snyk test ``` ### 9.2 Dependency Pinning ```json // package.json - use exact versions in production { "dependencies": { "react": "18.2.0", // Not "^18.2.0" "axios": "1.6.2" } } ``` --- ## 10. Deployment Security Checklist - [ ] All environment variables set correctly - [ ] JWT_SECRET is strong (32+ characters) - [ ] Database uses SSL/TLS - [ ] HTTPS enforced (no HTTP) - [ ] HSTS header enabled - [ ] CSP configured and tested - [ ] CSRF protection active - [ ] Rate limiting enabled - [ ] File upload limits configured - [ ] Backup encryption enabled - [ ] Log monitoring configured - [ ] Security headers verified - [ ] Dev/debug features disabled - [ ] Error messages don't leak sensitive info - [ ] Default credentials changed - [ ] API documentation not publicly accessible --- ## Quick Security Review Command ```bash # Run security checks ./scripts/security-check.sh ``` Create `scripts/security-check.sh`: ```bash #!/bin/bash echo "🔒 Security Check" echo "==================" # Check for hardcoded secrets echo "🔍 Checking for hardcoded secrets..." grep -r "password\s*=\s*['\"]" --include="*.go" --include="*.ts" --include="*.tsx" . || echo "✅ None found" # Check JWT secret in production echo "🔍 Checking JWT secret..." if [ "$APP_ENV" = "production" ] && [ "$JWT_SECRET" = "default-secret-key-change-in-production" ]; then echo "❌ CRITICAL: Default JWT secret in production!" else echo "✅ JWT secret OK" fi # Check HTTPS echo "🔍 Checking HTTPS enforcement..." # Add checks here # Check dependencies echo "🔍 Checking npm vulnerabilities..." cd frontend && npm audit --audit-level=high echo "✅ Security check complete" ``` --- ## Incident Response Plan 1. **Detect**: Monitor logs for suspicious activity 2. **Contain**: Disable compromised accounts/keys 3. **Investigate**: Review audit logs 4. **Remediate**: Apply patches, change secrets 5. **Document**: Record incident details 6. **Review**: Update security measures --- ## Resources - [OWASP Top 10](https://owasp.org/www-project-top-ten/) - [CWE Top 25](https://cwe.mitre.org/top25/) - [Go Security](https://go.dev/security/) - [React Security](https://react.dev/learn/security)