Add security hardening

This commit is contained in:
satoshi committed 2026-09-04 14:54:07 +03:00
1 parent 4669bf10c0
commit e592930728
4 files changed
+202 -9

No files matched your search

+107
View File
@@ -0,0 +1,107 @@
package httpapi
import (
"io"
"log/slog"
"net/http"
"net/http/httptest"
"testing"
)
func TestOriginAllowed(t *testing.T) {
tests := []struct {
name string
host string
origin string
want bool
}{
// No Origin: not browser-driven, so not a cross-site vector.
{"no origin header", "pool.local:8080", "", true},
{"same origin", "pool.local:8080", "http://pool.local:8080", true},
{"same origin over https", "pool.local", "https://pool.local", true},
{"case-insensitive host", "Pool.Local:8080", "http://pool.local:8080", true},
// The attacks this closes.
{"attacker site", "pool.local:8080", "https://evil.example", false},
{"different port on same name", "pool.local:8080", "http://pool.local:9999", false},
{"subdomain of the host", "pool.local:8080", "http://x.pool.local:8080", false},
{"suffix trick", "pool.local:8080", "http://notpool.local:8080", false},
{"null origin from a sandboxed frame", "pool.local:8080", "null", false},
{"malformed origin", "pool.local:8080", "::::", false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
r := httptest.NewRequest("POST", "http://"+tc.host+"/api/admin/reset-latency", nil)
r.Host = tc.host
if tc.origin != "" {
r.Header.Set("Origin", tc.origin)
}
if got := originAllowed(r); got != tc.want {
t.Errorf("originAllowed(host=%q origin=%q) = %v, want %v",
tc.host, tc.origin, got, tc.want)
}
})
}
}
// A cross-origin POST must be refused before the handler runs, so a page
// the operator happens to be visiting cannot drive the admin routes with
// their session.
func TestSameOriginRefusesCrossSite(t *testing.T) {
s := &Server{Log: discardLogger()}
called := false
h := s.sameOrigin(func(w http.ResponseWriter, r *http.Request) {
called = true
w.WriteHeader(http.StatusOK)
})
r := httptest.NewRequest("POST", "http://pool.local:8080/api/admin/reset-latency", nil)
r.Host = "pool.local:8080"
r.Header.Set("Origin", "https://evil.example")
w := httptest.NewRecorder()
h(w, r)
if called {
t.Error("handler ran for a cross-origin request")
}
if w.Code != http.StatusForbidden {
t.Errorf("status = %d, want 403", w.Code)
}
}
func TestSameOriginAllowsSameSite(t *testing.T) {
s := &Server{Log: discardLogger()}
called := false
h := s.sameOrigin(func(w http.ResponseWriter, r *http.Request) { called = true })
r := httptest.NewRequest("POST", "http://pool.local:8080/api/admin/ack-best", nil)
r.Host = "pool.local:8080"
r.Header.Set("Origin", "http://pool.local:8080")
h(httptest.NewRecorder(), r)
if !called {
t.Error("handler did not run for a same-origin request")
}
}
func TestSecurityHeaders(t *testing.T) {
h := securityHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
w := httptest.NewRecorder()
h.ServeHTTP(w, httptest.NewRequest("GET", "/", nil))
for k, want := range map[string]string{
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"Referrer-Policy": "no-referrer",
} {
if got := w.Header().Get(k); got != want {
t.Errorf("%s = %q, want %q", k, got, want)
}
}
}
func discardLogger() *slog.Logger {
return slog.New(slog.NewTextHandler(io.Discard, nil))
}