mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
598 lines
13 KiB
Markdown
598 lines
13 KiB
Markdown
# 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 <div dangerouslySetInnerHTML={{ __html: clean }} />;
|
|
};
|
|
```
|
|
|
|
### 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)
|