Add security hardening
This commit is contained in:
4 files changed
+202
-9
No files matched your search
@@ -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))
|
||||
}
|
||||
Reference in new issue
Block a user