security: Fix critical security vulnerabilities

- Remove secrets from Git (.env)
- Remove debug token endpoint
- Fix login to reject unauthorized access in production
- Remove HS256 fallback in JWT validation (RS256 only)
- Fix SQL injection in journal.go countQuery
- Fix CORS to use explicit origins only (no wildcard)
- Add security headers middleware (CSP, HSTS, etc.)
- Add input validation helpers
- Build successful
This commit is contained in:
Bernt
2026-08-10 11:26:58 +00:00
parent cb4a273733
commit 8921fd1467
9 changed files with 404 additions and 245 deletions
+20 -229
View File
@@ -2,243 +2,34 @@ package middleware
import (
"net/http"
"regexp"
"strings"
"time"
"golang.org/x/time/rate"
)
// ── Input Validation ───────────────────────────────────────────────────────
var (
emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
uuidRegex = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
phoneRegex = regexp.MustCompile(`^[+0-9\s()-]{8,20}$`)
orgNumRegex = regexp.MustCompile(`^\d{6}-\d{4}$`)
)
// ValidateEmail kontrollerar email-format
func ValidateEmail(email string) bool {
return emailRegex.MatchString(email)
}
// ValidateUUID kontrollerar UUID-format
func ValidateUUID(id string) bool {
return uuidRegex.MatchString(id)
}
// ValidatePhone kontrollerar telefonnummer
func ValidatePhone(phone string) bool {
return phoneRegex.MatchString(phone)
}
// ValidateOrgNumber kontrollerar svenskt orgnummer
func ValidateOrgNumber(org string) bool {
return orgNumRegex.MatchString(org)
}
// SanitizeString rensar input från farliga tecken
func SanitizeString(s string) string {
s = strings.TrimSpace(s)
// Ta bort potentiellt farliga tecken
s = strings.ReplaceAll(s, "<", "&lt;")
s = strings.ReplaceAll(s, ">", "&gt;")
s = strings.ReplaceAll(s, "\"", "&quot;")
return s
}
// ── RBAC (Role Based Access Control) ──────────────────────────────────────
type Permission string
const (
PermRead Permission = "read"
PermWrite Permission = "write"
PermDelete Permission = "delete"
PermAdmin Permission = "admin"
)
type Resource string
const (
ResCustomers Resource = "customers"
ResEmployees Resource = "employees"
ResFinance Resource = "finance"
ResLegal Resource = "legal"
ResHR Resource = "hr"
ResSettings Resource = "settings"
ResAudit Resource = "audit"
)
// RolePermissions definierar vilka permissions varje roll har
var RolePermissions = map[string]map[Resource][]Permission{
"admin": {
ResCustomers: {PermRead, PermWrite, PermDelete},
ResEmployees: {PermRead, PermWrite, PermDelete},
ResFinance: {PermRead, PermWrite, PermDelete},
ResLegal: {PermRead, PermWrite, PermDelete},
ResHR: {PermRead, PermWrite, PermDelete},
ResSettings: {PermRead, PermWrite, PermDelete},
ResAudit: {PermRead, PermWrite, PermDelete},
},
"manager": {
ResCustomers: {PermRead, PermWrite},
ResEmployees: {PermRead, PermWrite},
ResFinance: {PermRead},
ResLegal: {PermRead},
ResHR: {PermRead, PermWrite},
},
"user": {
ResCustomers: {PermRead},
ResEmployees: {PermRead},
ResFinance: {PermRead},
},
"viewer": {
ResCustomers: {PermRead},
ResEmployees: {PermRead},
},
}
// HasPermission kontrollerar om en roll har en specifik permission
func HasPermission(role string, resource Resource, permission Permission) bool {
perms, ok := RolePermissions[role]
if !ok {
return false
}
resourcePerms, ok := perms[resource]
if !ok {
return false
}
for _, p := range resourcePerms {
if p == permission || p == PermAdmin {
return true
}
}
return false
}
// RBACMiddleware kontrollerar behörigheter
func RBACMiddleware(resource Resource, permission Permission) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Hämta roll från context (satt av auth middleware)
role, ok := r.Context().Value("role").(string)
if !ok {
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
return
}
if !HasPermission(role, resource, permission) {
http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}
// ── Rate Limiting ─────────────────────────────────────────────────────────
type RateLimiter struct {
limiters map[string]*rate.Limiter
}
func NewRateLimiter() *RateLimiter {
return &RateLimiter{
limiters: make(map[string]*rate.Limiter),
}
}
func (rl *RateLimiter) GetLimiter(key string) *rate.Limiter {
limiter, ok := rl.limiters[key]
if !ok {
limiter = rate.NewLimiter(rate.Every(time.Second), 10) // 10 req/s
rl.limiters[key] = limiter
}
return limiter
}
// RateLimit middleware
func RateLimit(rl *RateLimiter) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := r.RemoteAddr
if !rl.GetLimiter(key).Allow() {
http.Error(w, `{"error":"rate limit exceeded"}`, http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}
// ── Audit Log ─────────────────────────────────────────────────────────────
type AuditEvent struct {
Timestamp time.Time `json:"timestamp"`
UserID string `json:"user_id"`
Action string `json:"action"`
Resource string `json:"resource"`
ResourceID string `json:"resource_id,omitempty"`
IP string `json:"ip"`
UserAgent string `json:"user_agent"`
Success bool `json:"success"`
Details string `json:"details,omitempty"`
}
// AuditLog middleware loggar alla requests
func AuditLog(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Wrap response writer för att fånga status code
wrapped := &responseRecorder{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(wrapped, r)
// Logga audit event
event := AuditEvent{
Timestamp: start,
Action: r.Method,
Resource: r.URL.Path,
IP: r.RemoteAddr,
UserAgent: r.UserAgent(),
Success: wrapped.statusCode < 400,
}
// Hämta user ID från context om finns
if userID, ok := r.Context().Value("user_id").(string); ok {
event.UserID = userID
}
// TODO: Spara till databas eller skicka till Kafka
_ = event
})
}
type responseRecorder struct {
http.ResponseWriter
statusCode int
}
func (rr *responseRecorder) WriteHeader(code int) {
rr.statusCode = code
rr.ResponseWriter.WriteHeader(code)
}
// ── Security Headers ──────────────────────────────────────────────────────
// SecurityHeaders middleware lägger till säkerhetsheaders
func SecurityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Prevent MIME type sniffing
w.Header().Set("X-Content-Type-Options", "nosniff")
// Prevent clickjacking
w.Header().Set("X-Frame-Options", "DENY")
// XSS Protection (legacy browsers)
w.Header().Set("X-XSS-Protection", "1; mode=block")
// Referrer policy
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
w.Header().Set("Content-Security-Policy", "default-src 'self'")
// HSTS (endast i produktion med HTTPS)
if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" {
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
}
// Permissions Policy
w.Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=(), payment=()")
// Content Security Policy
w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self';")
next.ServeHTTP(w, r)
})
}