Files
boc/backend/middleware/validation.go
T
Bernt 8921fd1467 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
2026-08-10 11:26:58 +00:00

282 lines
7.1 KiB
Go

package middleware
import (
"context"
"encoding/json"
"fmt"
"net/http"
"reflect"
"regexp"
"strconv"
"strings"
)
var (
emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
uuidRegex = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
)
// ValidateEmail kontrollerar email-format
func IsValidEmail(email string) bool {
return emailRegex.MatchString(email)
}
// ValidateUUID kontrollerar UUID-format (legacy alias)
func ValidateUUID(uuid string) bool {
return uuidRegex.MatchString(uuid)
}
// ValidateEmail kontrollerar email-format (legacy alias)
func ValidateEmail(email string) bool {
return emailRegex.MatchString(email)
}
// ValidatePhone kontrollerar telefonnummer
func ValidatePhone(phone string) bool {
// Tillåt +, siffror, mellanslag och bindestreck
cleaned := strings.ReplaceAll(phone, " ", "")
cleaned = strings.ReplaceAll(cleaned, "-", "")
return len(cleaned) >= 8 && len(cleaned) <= 15
}
// ValidateOrgNumber kontrollerar svenskt organisationsnummer
func ValidateOrgNumber(org string) bool {
// Ta bort mellanslag och bindestreck
cleaned := strings.ReplaceAll(org, " ", "")
cleaned = strings.ReplaceAll(cleaned, "-", "")
if len(cleaned) != 10 {
return false
}
// Kontrollera att det bara är siffror
for _, c := range cleaned {
if c < '0' || c > '9' {
return false
}
}
return true
}
// SanitizeString tar bort farliga tecken från strängar
func SanitizeString(s string) string {
// Ta bort null bytes och kontrolltecken
var result strings.Builder
for _, r := range s {
if r >= 32 || r == '\t' || r == '\n' || r == '\r' {
result.WriteRune(r)
}
}
return strings.TrimSpace(result.String())
}
// ── Request Validation ────────────────────────────────────────────────────
type Validator struct {
errors map[string][]string
}
func NewValidator() *Validator {
return &Validator{errors: make(map[string][]string)}
}
func (v *Validator) AddError(field, message string) {
v.errors[field] = append(v.errors[field], message)
}
func (v *Validator) HasErrors() bool {
return len(v.errors) > 0
}
func (v *Validator) Errors() map[string][]string {
return v.errors
}
func (v *Validator) ErrorResponse() map[string]interface{} {
return map[string]interface{}{
"error": "validation failed",
"details": v.errors,
}
}
// ValidateString kontrollerar strängfält
func (v *Validator) ValidateString(field, value string, minLen, maxLen int, required bool) {
if required && strings.TrimSpace(value) == "" {
v.AddError(field, "is required")
return
}
if value != "" {
if len(value) < minLen {
v.AddError(field, fmt.Sprintf("must be at least %d characters", minLen))
}
if len(value) > maxLen {
v.AddError(field, fmt.Sprintf("must be at most %d characters", maxLen))
}
}
}
// ValidateEmail kontrollerar email
func (v *Validator) ValidateEmail(field, value string, required bool) {
if required && strings.TrimSpace(value) == "" {
v.AddError(field, "is required")
return
}
if value != "" && !IsValidEmail(value) {
v.AddError(field, "invalid email format")
}
}
// ValidateUUID kontrollerar UUID
func (v *Validator) ValidateUUID(field, value string, required bool) {
if required && strings.TrimSpace(value) == "" {
v.AddError(field, "is required")
return
}
if value != "" && !ValidateUUID(value) {
v.AddError(field, "invalid UUID format")
}
}
// ValidateInt kontrollerar heltal
func (v *Validator) ValidateInt(field string, value int, min, max int, required bool) {
if required && value == 0 {
v.AddError(field, "is required")
return
}
if value != 0 {
if value < min {
v.AddError(field, fmt.Sprintf("must be at least %d", min))
}
if value > max {
v.AddError(field, fmt.Sprintf("must be at most %d", max))
}
}
}
// ValidateFloat kontrollerar decimaltal
func (v *Validator) ValidateFloat(field string, value float64, min, max float64, required bool) {
if required && value == 0 {
v.AddError(field, "is required")
return
}
if value != 0 {
if value < min {
v.AddError(field, fmt.Sprintf("must be at least %.2f", min))
}
if value > max {
v.AddError(field, fmt.Sprintf("must be at most %.2f", max))
}
}
}
// ValidateEnum kontrollerar att värdet finns i tillåtna värden
func (v *Validator) ValidateEnum(field, value string, allowed []string, required bool) {
if required && strings.TrimSpace(value) == "" {
v.AddError(field, "is required")
return
}
if value != "" {
found := false
for _, a := range allowed {
if a == value {
found = true
break
}
}
if !found {
v.AddError(field, fmt.Sprintf("must be one of: %s", strings.Join(allowed, ", ")))
}
}
}
// ── Validation Middleware ─────────────────────────────────────────────────
// ValidateBody validerar request body mot en struct
func ValidateBody(dst interface{}) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Body == nil {
http.Error(w, `{"error":"request body required"}`, http.StatusBadRequest)
return
}
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(dst); err != nil {
http.Error(w, fmt.Sprintf(`{"error":"invalid request body: %s"}`, err.Error()), http.StatusBadRequest)
return
}
// Validera fält
validator := NewValidator()
validateStruct(validator, dst)
if validator.HasErrors() {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(validator.ErrorResponse())
return
}
// Spara validerad struct i context
ctx := context.WithValue(r.Context(), "validated_body", dst)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// validateStruct validerar en struct baserat på tags
func validateStruct(v *Validator, s interface{}) {
val := reflect.ValueOf(s)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
fieldType := typ.Field(i)
// Hämta validation tags
tag := fieldType.Tag.Get("validate")
if tag == "" {
continue
}
// Parsa tag
parts := strings.Split(tag, ",")
required := false
minLen := 0
maxLen := 255
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "required" {
required = true
} else if strings.HasPrefix(part, "min=") {
minLen, _ = strconv.Atoi(strings.TrimPrefix(part, "min="))
} else if strings.HasPrefix(part, "max=") {
maxLen, _ = strconv.Atoi(strings.TrimPrefix(part, "max="))
}
}
// Validera baserat på typ
switch field.Kind() {
case reflect.String:
v.ValidateString(fieldType.Name, field.String(), minLen, maxLen, required)
case reflect.Int, reflect.Int64:
v.ValidateInt(fieldType.Name, int(field.Int()), 0, 999999, required)
case reflect.Float64:
v.ValidateFloat(fieldType.Name, field.Float(), 0, 999999999, required)
}
}
}