GET ?password= and X-Paste-Password header checks in handleGetPaste were unlimited (pentest2: 8 wrong = 8x401, no 429). Gate every password verification on the same per-IP+paste unlock limiter (5/min, 429 with Retry-After) already used by the POST unlock form path. Regression tests: repeated wrong passwords via GET ?password= and via header must eventually 429; correct password within burst still 200. Fixes #81
93 lines
2.6 KiB
Go
93 lines
2.6 KiB
Go
package api
|
|
|
|
// #81: ALL password verification attempts (GET query param, header, POST
|
|
// form) must go through the per-IP unlock limiter. Regression: N wrong
|
|
// passwords via GET ?password= must eventually yield 429.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func createPasswordPaste(t *testing.T, s *apiServer, pw string) string {
|
|
t.Helper()
|
|
h := s.routes()
|
|
body := `{"content":"secret","password":"` + pw + `"}`
|
|
req := httptest.NewRequest("POST", "/api/pastes", strings.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
var created struct {
|
|
ID string `json:"id"`
|
|
}
|
|
json.Unmarshal(rec.Body.Bytes(), &created)
|
|
return created.ID
|
|
}
|
|
|
|
// TestRateLimitGetPasswordQuery: repeated wrong passwords via GET
|
|
// ?password= must eventually return 429 (unlock limiter: burst 5).
|
|
func TestRateLimitGetPasswordQuery(t *testing.T) {
|
|
s := testServer(t)
|
|
h := s.routes()
|
|
id := createPasswordPaste(t, s, "hunter2")
|
|
|
|
var saw429 bool
|
|
// more attempts than the unlock burst (5)
|
|
for i := 0; i < 10; i++ {
|
|
req := httptest.NewRequest("GET", "/api/pastes/"+id+"?password=wrong"+string(rune('a'+i)), nil)
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code == 429 {
|
|
saw429 = true
|
|
break
|
|
}
|
|
if rec.Code != 401 {
|
|
t.Fatalf("attempt %d: expected 401 before limit, got %d", i, rec.Code)
|
|
}
|
|
}
|
|
if !saw429 {
|
|
t.Fatal("expected 429 after repeated wrong ?password= attempts, never got one")
|
|
}
|
|
}
|
|
|
|
// TestRateLimitGetPasswordHeader: same guarantee for the X-Paste-Password header path.
|
|
func TestRateLimitGetPasswordHeader(t *testing.T) {
|
|
s := testServer(t)
|
|
h := s.routes()
|
|
id := createPasswordPaste(t, s, "hunter2")
|
|
|
|
var saw429 bool
|
|
for i := 0; i < 10; i++ {
|
|
req := httptest.NewRequest("GET", "/api/pastes/"+id, nil)
|
|
req.Header.Set("X-Paste-Password", "wrong"+string(rune('a'+i)))
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code == 429 {
|
|
saw429 = true
|
|
break
|
|
}
|
|
if rec.Code != 401 {
|
|
t.Fatalf("attempt %d: expected 401 before limit, got %d", i, rec.Code)
|
|
}
|
|
}
|
|
if !saw429 {
|
|
t.Fatal("expected 429 after repeated wrong header password attempts, never got one")
|
|
}
|
|
}
|
|
|
|
// TestRateLimitGetPasswordCorrectStillAllowed: a correct password must still
|
|
// work within the burst (the limiter gates attempts, not correctness).
|
|
func TestRateLimitGetPasswordCorrectStillAllowed(t *testing.T) {
|
|
s := testServer(t)
|
|
h := s.routes()
|
|
id := createPasswordPaste(t, s, "hunter2")
|
|
|
|
req := httptest.NewRequest("GET", "/api/pastes/"+id+"?password=hunter2", nil)
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code != 200 {
|
|
t.Fatalf("expected 200 for correct password within burst, got %d", rec.Code)
|
|
}
|
|
}
|