Files
MyClub/DOCS/SECURITY_BEST_PRACTICES.md
T
Tomáš Dvořák 12cba639b9 upload
2025-10-16 13:32:05 +02:00

13 KiB

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:

// 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

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

// 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

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

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

// 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

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

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

// 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)

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

// Use SSL for database connection in production
if config.AppConfig.AppEnv == "production" {
    dsn += "?sslmode=require"
}

5.2 Sensitive Data Encryption

// 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

# Encrypt database backups
pg_dump dbname | gpg -c > backup.sql.gpg

6. Logging & Monitoring

6.1 Security Event Logging

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

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

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

# 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

// 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 <div dangerouslySetInnerHTML={{ __html: clean }} />;
};

8.2 Sensitive Data in LocalStorage

// 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

// 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

# 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

// 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

# Run security checks
./scripts/security-check.sh

Create scripts/security-check.sh:

#!/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