aboutsummaryrefslogtreecommitdiffstats
path: root/internal/web/web_test.go
diff options
context:
space:
mode:
authorgrm <grm@eyesin.space>2026-09-18 13:46:11 +0300
committergrm <grm@eyesin.space>2026-09-18 13:46:11 +0300
commitf8d90e3d80ec798689be5fdf792e6c1401ad748f (patch)
tree80e37791696168eb09ccccdec37bfc0f75e4f52d /internal/web/web_test.go
parent90578f02d851ab4e28a066404fbcf4be6a0ed9a7 (diff)
downloadblogspace-f8d90e3d80ec798689be5fdf792e6c1401ad748f.tar.gz
blogspace-f8d90e3d80ec798689be5fdf792e6c1401ad748f.tar.bz2
blogspace-f8d90e3d80ec798689be5fdf792e6c1401ad748f.zip
Security: Throttle login attempts with a per-address, per-account token bucket
Nothing stopped a bot from trying passwords against /webadmin as fast as bcrypt would go. A small in-memory limiter (stdlib only, one process) now refuses a login with 429 once an address, or an account, has made ten attempts, and lets one more through every six seconds; keying on both means many addresses guessing one account are throttled too. Refusals are logged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
Diffstat (limited to 'internal/web/web_test.go')
-rw-r--r--internal/web/web_test.go42
1 files changed, 42 insertions, 0 deletions
diff --git a/internal/web/web_test.go b/internal/web/web_test.go
index 1d4db4c..6324443 100644
--- a/internal/web/web_test.go
+++ b/internal/web/web_test.go
@@ -920,3 +920,45 @@ func TestSingleRangeOnly(t *testing.T) {
}
}
}
+
+func TestLimiter(t *testing.T) {
+ now := time.Unix(0, 0)
+ l := newLimiter(60, 3) // one a second, three at once
+ l.now = func() time.Time { return now }
+ for i := 0; i < 3; i++ {
+ if !l.allow("a") {
+ t.Fatalf("burst request %d refused", i)
+ }
+ }
+ if l.allow("a") {
+ t.Error("fourth request allowed")
+ }
+ if !l.allow("b") {
+ t.Error("another key shares the bucket")
+ }
+ now = now.Add(time.Second)
+ if !l.allow("a") || l.allow("a") {
+ t.Error("refill is not one token per second")
+ }
+ now = now.Add(time.Hour) // idle buckets are forgotten at the sweep
+ l.allow("c")
+ if _, ok := l.buckets["a"]; ok {
+ t.Error("full bucket kept after the sweep")
+ }
+}
+
+// A guessed password is refused with 429 once the address or the account has used its burst.
+func TestLoginThrottled(t *testing.T) {
+ s := NewServer(&config.Config{BaseDomain: "example.com", JWTSecret: []byte("x")}, nil)
+ for i := 0; i < 10; i++ {
+ s.loginLimit.allow("user:admin")
+ }
+ rec := httptest.NewRecorder()
+ req := httptest.NewRequest("POST", "/webadmin", strings.NewReader("username=Admin&password=x"))
+ req.Host = "example.com"
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ s.ServeHTTP(rec, req)
+ if rec.Code != http.StatusTooManyRequests {
+ t.Errorf("got %d, want 429", rec.Code)
+ }
+}