Rely on loopback bind plus Host/Origin checks; open http://127.0.0.1:8787/ without a query token after SSH tunnel. Co-authored-by: Cursor <cursoragent@cursor.com>
158 lines
4.7 KiB
Go
158 lines
4.7 KiB
Go
package engineeringdashboard
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestServer_RejectsNonGETAndBadHost(t *testing.T) {
|
|
c := NewCollector(t.TempDir(), &FakeRunner{Map: map[string]FakeResult{}})
|
|
c.mu.Lock()
|
|
c.current = &Snapshot{
|
|
SchemaVersion: SchemaVersion, GenerationID: "gen_1", RevisionID: "rev_1",
|
|
GeneratedAt: time.Now().UTC(), Repository: RepositoryIdentity{Root: "/r"},
|
|
}
|
|
c.mu.Unlock()
|
|
srv, err := NewServer(c)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h := srv.Handler()
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/snapshot", nil)
|
|
req.Host = "127.0.0.1:8787"
|
|
rr := httptest.NewRecorder()
|
|
h.ServeHTTP(rr, req)
|
|
if rr.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("POST: %d", rr.Code)
|
|
}
|
|
|
|
req = httptest.NewRequest(http.MethodGet, "/api/v1/snapshot", nil)
|
|
req.Host = "evil.example"
|
|
rr = httptest.NewRecorder()
|
|
h.ServeHTTP(rr, req)
|
|
if rr.Code != http.StatusForbidden {
|
|
t.Fatalf("bad host: %d", rr.Code)
|
|
}
|
|
}
|
|
|
|
func TestServer_SnapshotWithoutToken(t *testing.T) {
|
|
c := NewCollector(t.TempDir(), &FakeRunner{Map: map[string]FakeResult{}})
|
|
snap := Snapshot{
|
|
SchemaVersion: SchemaVersion, GenerationID: "gen_9", RevisionID: "rev_abc",
|
|
GeneratedAt: time.Now().UTC(), Repository: RepositoryIdentity{Root: "/repo", Branch: "dev", Head: "h"},
|
|
Tasks: []TaskState{SampleRDevTaskFixture()},
|
|
}
|
|
c.mu.Lock()
|
|
c.current = &snap
|
|
c.mu.Unlock()
|
|
srv, err := NewServer(c)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h := srv.Handler()
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/snapshot", nil)
|
|
req.Host = "127.0.0.1:8787"
|
|
rr := httptest.NewRecorder()
|
|
h.ServeHTTP(rr, req)
|
|
if rr.Code != 200 {
|
|
t.Fatalf("snapshot without token: %d %s", rr.Code, rr.Body.String())
|
|
}
|
|
body := rr.Body.String()
|
|
if !strings.Contains(body, `"generation_id": "gen_9"`) || !strings.Contains(body, `"revision_id": "rev_abc"`) {
|
|
t.Fatalf("identity missing: %s", body)
|
|
}
|
|
if rr.Header().Get("ETag") != `"rev_abc"` {
|
|
t.Fatalf("etag %q", rr.Header().Get("ETag"))
|
|
}
|
|
|
|
req = httptest.NewRequest(http.MethodGet, "/api/v1/snapshot", nil)
|
|
req.Host = "127.0.0.1:8787"
|
|
req.Header.Set("If-None-Match", `"rev_abc"`)
|
|
rr = httptest.NewRecorder()
|
|
h.ServeHTTP(rr, req)
|
|
if rr.Code != http.StatusNotModified {
|
|
t.Fatalf("etag: %d", rr.Code)
|
|
}
|
|
}
|
|
|
|
func TestServer_DocPathTraversalDenied(t *testing.T) {
|
|
root := t.TempDir()
|
|
writeDoc(t, root, ".doc/ops/ok.md", "# ok\n")
|
|
c := NewCollector(root, &FakeRunner{Map: map[string]FakeResult{}})
|
|
srv, err := NewServer(c)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/doc/not-a-real.md", nil)
|
|
req.Host = "127.0.0.1:8787"
|
|
req.URL.Path = "/api/v1/doc/../../.env"
|
|
rr := httptest.NewRecorder()
|
|
srv.Handler().ServeHTTP(rr, req)
|
|
if rr.Code != http.StatusForbidden {
|
|
t.Fatalf("got %d body=%s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestListenAndServe_RejectsNonLoopback(t *testing.T) {
|
|
err := ListenAndServe("0.0.0.0:8787", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
|
if err == nil || !strings.Contains(err.Error(), "loopback") {
|
|
t.Fatalf("expected loopback reject, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestServer_CrossOriginDenied(t *testing.T) {
|
|
c := NewCollector(t.TempDir(), &FakeRunner{})
|
|
c.mu.Lock()
|
|
c.current = &Snapshot{RevisionID: "r", GenerationID: "g", GeneratedAt: time.Now().UTC()}
|
|
c.mu.Unlock()
|
|
srv, _ := NewServer(c)
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/snapshot", nil)
|
|
req.Host = "127.0.0.1:8787"
|
|
req.Header.Set("Origin", "https://evil.example")
|
|
rr := httptest.NewRecorder()
|
|
srv.Handler().ServeHTTP(rr, req)
|
|
if rr.Code != http.StatusForbidden {
|
|
t.Fatalf("got %d", rr.Code)
|
|
}
|
|
}
|
|
|
|
func TestServer_IndexHasNoTokenMachinery(t *testing.T) {
|
|
c := NewCollector(t.TempDir(), &FakeRunner{})
|
|
c.mu.Lock()
|
|
c.current = &Snapshot{RevisionID: "r", GenerationID: "g", GeneratedAt: time.Now().UTC()}
|
|
c.mu.Unlock()
|
|
srv, err := NewServer(c)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req.Host = "127.0.0.1:8787"
|
|
rr := httptest.NewRecorder()
|
|
srv.Handler().ServeHTTP(rr, req)
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("index: %d %s", rr.Code, rr.Body.String())
|
|
}
|
|
body := rr.Body.String()
|
|
for _, banned := range []string{"__DASHBOARD_TOKEN__", "engineering-dashboard-token", "?token="} {
|
|
if strings.Contains(body, banned) {
|
|
t.Fatalf("index still contains %q", banned)
|
|
}
|
|
}
|
|
js, err := readAsset("assets/app.js")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
jsBody := string(js)
|
|
if strings.Contains(jsBody, "token=") || strings.Contains(jsBody, "__DASHBOARD_TOKEN__") {
|
|
t.Fatal("app.js still references token auth")
|
|
}
|
|
if !strings.Contains(rr.Header().Get("Content-Security-Policy"), "script-src 'self'") {
|
|
t.Fatalf("unexpected CSP: %s", rr.Header().Get("Content-Security-Policy"))
|
|
}
|
|
}
|