feat(security): add trusted proxies and abuse event cleanup
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m38s
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.
This commit is contained in:
117
backend/libs/services/bans_test.go
Normal file
117
backend/libs/services/bans_test.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestBanServiceMatchesIPAndCIDR(t *testing.T) {
|
||||
bans := newTestBanService(t)
|
||||
now := time.Date(2026, 5, 31, 12, 0, 0, 0, time.UTC)
|
||||
ipBan, err := bans.createBan("203.0.113.5", "single IP", BanSourceManual, "test", now.Add(time.Hour), now)
|
||||
if err != nil {
|
||||
t.Fatalf("createBan IP returned error: %v", err)
|
||||
}
|
||||
cidrBan, err := bans.createBan("198.51.100.0/24", "CIDR", BanSourceManual, "test", now.Add(time.Hour), now)
|
||||
if err != nil {
|
||||
t.Fatalf("createBan CIDR returned error: %v", err)
|
||||
}
|
||||
|
||||
if matched, ok, err := bans.Match("203.0.113.5", now); err != nil || !ok || matched.Ban.ID != ipBan.ID {
|
||||
t.Fatalf("Match IP = %+v, %v, %v", matched, ok, err)
|
||||
}
|
||||
if matched, ok, err := bans.Match("198.51.100.42", now); err != nil || !ok || matched.Ban.ID != cidrBan.ID {
|
||||
t.Fatalf("Match CIDR = %+v, %v, %v", matched, ok, err)
|
||||
}
|
||||
if _, ok, err := bans.Match("192.0.2.1", now); err != nil || ok {
|
||||
t.Fatalf("Match unrelated = %v, %v", ok, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBanServiceIgnoresExpiredAndUnbanned(t *testing.T) {
|
||||
bans := newTestBanService(t)
|
||||
now := time.Date(2026, 5, 31, 12, 0, 0, 0, time.UTC)
|
||||
expired, err := bans.createBan("203.0.113.6", "expired", BanSourceManual, "test", now.Add(time.Hour), now)
|
||||
if err != nil {
|
||||
t.Fatalf("createBan expired returned error: %v", err)
|
||||
}
|
||||
if _, ok, err := bans.Match("203.0.113.6", now.Add(2*time.Hour)); err != nil || ok {
|
||||
t.Fatalf("expired Match = %v, %v", ok, err)
|
||||
}
|
||||
active, err := bans.createBan("203.0.113.7", "active", BanSourceManual, "test", now.Add(time.Hour), now)
|
||||
if err != nil {
|
||||
t.Fatalf("createBan active returned error: %v", err)
|
||||
}
|
||||
if err := bans.Unban(active.ID, now.Add(time.Minute)); err != nil {
|
||||
t.Fatalf("Unban returned error: %v", err)
|
||||
}
|
||||
if _, ok, err := bans.Match("203.0.113.7", now.Add(2*time.Minute)); err != nil || ok {
|
||||
t.Fatalf("unbanned Match = %v, %v", ok, err)
|
||||
}
|
||||
if expired.Status(now.Add(2*time.Hour)) != "expired" {
|
||||
t.Fatalf("expired status = %q", expired.Status(now.Add(2*time.Hour)))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBanServiceAutoBanThresholdsAndDisabled(t *testing.T) {
|
||||
bans := newTestBanService(t)
|
||||
now := time.Date(2026, 5, 31, 12, 0, 0, 0, time.UTC)
|
||||
if result, err := bans.RecordAbuse("203.0.113.8", AbuseKindMaliciousPath, "/.env", 3, now); err != nil || result.Enabled {
|
||||
t.Fatalf("disabled RecordAbuse = %+v, %v", result, err)
|
||||
}
|
||||
settings, err := bans.Settings()
|
||||
if err != nil {
|
||||
t.Fatalf("Settings returned error: %v", err)
|
||||
}
|
||||
settings.AutoBanEnabled = true
|
||||
if err := bans.UpdateSettings(settings); err != nil {
|
||||
t.Fatalf("UpdateSettings returned error: %v", err)
|
||||
}
|
||||
for i := 0; i < 2; i++ {
|
||||
result, err := bans.RecordAbuse("203.0.113.8", AbuseKindMaliciousPath, "/.env", 3, now.Add(time.Duration(i)*time.Minute))
|
||||
if err != nil || result.Triggered {
|
||||
t.Fatalf("RecordAbuse before threshold = %+v, %v", result, err)
|
||||
}
|
||||
}
|
||||
result, err := bans.RecordAbuse("203.0.113.8", AbuseKindMaliciousPath, "/.env", 3, now.Add(3*time.Minute))
|
||||
if err != nil || !result.Triggered || result.Ban.ID == "" {
|
||||
t.Fatalf("RecordAbuse threshold = %+v, %v", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBanServiceMaliciousPathRules(t *testing.T) {
|
||||
bans := newTestBanService(t)
|
||||
if pattern, err := bans.MaliciousPattern("/foo/.ENV"); err != nil || pattern == "" {
|
||||
t.Fatalf("MaliciousPattern .env = %q, %v", pattern, err)
|
||||
}
|
||||
if pattern, err := bans.MaliciousPattern("/static/.env"); err != nil || pattern != "" {
|
||||
t.Fatalf("MaliciousPattern static = %q, %v", pattern, err)
|
||||
}
|
||||
if err := bans.SaveRules([]string{"/custom-probe"}, time.Now().UTC()); err != nil {
|
||||
t.Fatalf("SaveRules returned error: %v", err)
|
||||
}
|
||||
if pattern, err := bans.MaliciousPattern("/x/CUSTOM-probe"); err != nil || pattern != "/custom-probe" {
|
||||
t.Fatalf("MaliciousPattern custom = %q, %v", pattern, err)
|
||||
}
|
||||
}
|
||||
|
||||
func newTestBanService(t *testing.T) *BanService {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
upload, err := NewUploadService(1024*1024, filepath.Join(root, "data"), "http://example.test", slog.Default())
|
||||
if err != nil {
|
||||
t.Fatalf("NewUploadService returned error: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := upload.Close(); err != nil {
|
||||
t.Fatalf("Close returned error: %v", err)
|
||||
}
|
||||
})
|
||||
bans, err := NewBanService(upload.DB())
|
||||
if err != nil {
|
||||
t.Fatalf("NewBanService returned error: %v", err)
|
||||
}
|
||||
return bans
|
||||
}
|
||||
Reference in New Issue
Block a user