Query strings leak into Traefik access logs, browser history, and Referer headers. Admin key is now accepted only via X-Admin-Key; paste and can passwords only via X-Paste-Password (or the POST unlock form). Tests updated; new negative cases assert 401 for the query paths.
95 lines
2.7 KiB
Go
95 lines
2.7 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 X-Paste-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
|
|
// X-Paste-Password wrong attempts 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, 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 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, nil)
|
|
req.Header.Set("X-Paste-Password", "hunter2")
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
if rec.Code != 200 {
|
|
t.Fatalf("expected 200 for correct password within burst, got %d", rec.Code)
|
|
}
|
|
}
|