81 lines
2.5 KiB
Go
81 lines
2.5 KiB
Go
package api
|
|
|
|
// #66: admin key attempts must be rate limited per IP (5/min), constant-time
|
|
// compared, and failures logged. Hammering bad keys must yield 429s.
|
|
|
|
import (
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestAdminKeyRateLimited: burst of 5 bad-key attempts allowed (401), the 6th
|
|
// gets 429, and even the correct key is blocked from that IP until refill.
|
|
func TestAdminKeyRateLimited(t *testing.T) {
|
|
srv := newTestServer(t)
|
|
h := srv.routes()
|
|
reqIP := "10.7.7.1:1234"
|
|
|
|
var got429, retryAfter bool
|
|
var lastCode int
|
|
for i := 0; i < 10; i++ {
|
|
req := httptest.NewRequest("POST", "/admin/api/settings", nil)
|
|
req.RemoteAddr = reqIP
|
|
req.Header.Set("X-Admin-Key", "wrong-key")
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
lastCode = rec.Code
|
|
if rec.Code == 429 {
|
|
got429 = true
|
|
retryAfter = rec.Header().Get("Retry-After") != ""
|
|
break
|
|
}
|
|
}
|
|
if !got429 {
|
|
t.Fatalf("expected 429 after hammering bad keys, last status %d", lastCode)
|
|
}
|
|
if !retryAfter {
|
|
t.Error("429 missing Retry-After header")
|
|
}
|
|
|
|
// Correct key from the same IP is also locked out.
|
|
req := httptest.NewRequest("POST", "/admin/api/settings", nil)
|
|
req.RemoteAddr = reqIP
|
|
req.Header.Set("X-Admin-Key", srv.adminKey)
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code != 429 {
|
|
t.Errorf("correct key after lockout: got %d, want 429", rec.Code)
|
|
}
|
|
|
|
// A different IP is unaffected.
|
|
req2 := httptest.NewRequest("POST", "/admin/api/settings", strings.NewReader(`{"rate_limit_burst":5,"rate_limit_per_minute":60,"max_content_bytes":1048576,"custom_slug_reservation_days":30,"burn_viewer_window_minutes":15}`))
|
|
req2.RemoteAddr = "203.0.113.9:1234"
|
|
req2.Header.Set("X-Admin-Key", srv.adminKey)
|
|
rec2 := httptest.NewRecorder()
|
|
h.ServeHTTP(rec2, req2)
|
|
if rec2.Code != 200 {
|
|
t.Errorf("correct key from another IP: got %d, want 200", rec2.Code)
|
|
}
|
|
}
|
|
|
|
// TestAdminKeyConstantTimeCompare: sanity check that the comparison is
|
|
// constant-time (uses subtle.ConstantTimeCompare, not ==).
|
|
func TestAdminKeyConstantTimeCompare(t *testing.T) {
|
|
srv := newTestServer(t)
|
|
r := httptest.NewRequest("GET", "/", nil)
|
|
r.Header.Set("X-Admin-Key", "test-admin-key")
|
|
if !srv.adminKeyOK(r, srv.adminKey) {
|
|
t.Fatal("correct key rejected")
|
|
}
|
|
r.Header.Set("X-Admin-Key", "wrong")
|
|
if srv.adminKeyOK(r, srv.adminKey) {
|
|
t.Fatal("wrong key accepted")
|
|
}
|
|
// differ in length: must not panic/mismatch unexpectedly
|
|
r.Header.Set("X-Admin-Key", "test-admin-key-longer")
|
|
if srv.adminKeyOK(r, srv.adminKey) {
|
|
t.Fatal("longer wrong key accepted")
|
|
}
|
|
}
|