All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m38s
- Add `WARPBOX_TRUSTED_PROXIES` configuration to restrict accepted forwarded client IP headers to specific proxy IPs/CIDRs, securing client IP resolution. - Integrate `BanService` into the background cleanup job to automatically purge expired abuse and ban evidence events. - Update documentation with reverse proxy security guidelines and a production systemd deployment guide.
66 lines
2.5 KiB
Go
66 lines
2.5 KiB
Go
package middleware
|
|
|
|
import (
|
|
"log/slog"
|
|
"net/http"
|
|
"time"
|
|
|
|
"warpbox.dev/backend/libs/services"
|
|
)
|
|
|
|
func Bans(logger *slog.Logger, bans *services.BanService, trustedProxies []string) Middleware {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
ip := services.ClientIP(r.RemoteAddr, r.Header.Get("X-Forwarded-For"), r.Header.Get("X-Real-IP"), trustedProxies)
|
|
r = services.WithClientIP(r, ip)
|
|
now := time.Now().UTC()
|
|
|
|
if bans != nil {
|
|
if matched, ok, err := bans.Match(ip, now); err != nil {
|
|
logger.Error("ban match failed", "source", "ban", "severity", "error", "code", 5001, "ip", ip, "error", err.Error())
|
|
} else if ok {
|
|
logger.Warn("banned request blocked", "source", "ban", "severity", "warn", "code", 4030, "ip", ip, "ban_id", matched.Ban.ID, "target", matched.Ban.Normalized, "path", r.URL.Path)
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
w.WriteHeader(http.StatusForbidden)
|
|
_, _ = w.Write([]byte("forbidden\n"))
|
|
return
|
|
}
|
|
|
|
if pattern, err := bans.MaliciousPattern(r.URL.Path); err != nil {
|
|
logger.Error("malicious path check failed", "source", "ban", "severity", "error", "code", 5002, "ip", ip, "error", err.Error())
|
|
} else if pattern != "" {
|
|
if result, err := bans.RecordAbuse(ip, services.AbuseKindMaliciousPath, r.URL.Path, banThreshold(bans, services.AbuseKindMaliciousPath), now); err != nil {
|
|
logger.Error("malicious path event failed", "source", "ban", "severity", "error", "code", 5003, "ip", ip, "path", r.URL.Path, "error", err.Error())
|
|
} else if result.Enabled {
|
|
logger.Warn("malicious path requested", "source", "ban", "severity", "warn", "code", 4302, "ip", ip, "path", r.URL.Path, "pattern", pattern, "count", result.Event.Count)
|
|
if result.Triggered {
|
|
logger.Warn("ip auto-banned for malicious path", "source", "ban", "severity", "warn", "code", 4303, "ip", ip, "ban_id", result.Ban.ID, "path", r.URL.Path)
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
w.WriteHeader(http.StatusForbidden)
|
|
_, _ = w.Write([]byte("forbidden\n"))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
func banThreshold(bans *services.BanService, kind string) int {
|
|
settings, err := bans.Settings()
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
switch kind {
|
|
case services.AbuseKindAdminLogin:
|
|
return settings.AdminLoginFailureThreshold
|
|
case services.AbuseKindUserLogin:
|
|
return settings.UserLoginFailureThreshold
|
|
default:
|
|
return settings.MaliciousPathThreshold
|
|
}
|
|
}
|