mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
upload
This commit is contained in:
@@ -0,0 +1,597 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user