Scope jobs and snapshots to a browser session

An HttpOnly, SameSite=Lax session cookie is issued when a job is
created. Job and snapshot routes return 404 to any other session, so
indexed source is only readable by the browser that loaded it.
This commit is contained in:
Andy Dixon
2026-02-15 10:53:01 +00:00
parent d8d4a77ba7
commit 26800b9b66
3 changed files with 71 additions and 8 deletions
+36 -5
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"net/http/httptest"
"os"
"path/filepath"
@@ -67,14 +68,21 @@ func TestPathValidation(t *testing.T) {
}
type client struct {
t *testing.T
url string
t *testing.T
url string
http *http.Client
}
// stranger is a different browser session against the same server.
func (c client) stranger() client {
jar, _ := cookiejar.New(nil)
return client{c.t, c.url, &http.Client{Jar: jar}}
}
func (c client) do(method, path, body string) (int, []byte) {
c.t.Helper()
req, _ := http.NewRequest(method, c.url+path, strings.NewReader(body))
res, err := http.DefaultClient.Do(req)
res, err := c.http.Do(req)
if err != nil {
c.t.Fatal(err)
}
@@ -100,7 +108,7 @@ func newTestServer(t *testing.T) (*Server, client) {
server := NewServer(filepath.Join(t.TempDir(), "codenavigator"))
ts := httptest.NewServer(server)
t.Cleanup(ts.Close)
return server, client{t, ts.URL}
return server, client{t, ts.URL, nil}.stranger()
}
func TestLocalJobLifecycle(t *testing.T) {
@@ -118,7 +126,7 @@ func TestLocalJobLifecycle(t *testing.T) {
c.json("POST", "/api/jobs/"+created.JobID+"/files?path=..%2Fescape.js", "x", 400, nil)
c.json("POST", "/api/jobs/"+created.JobID+"/commit", "", 202, nil)
res, err := http.Get(c.url + "/api/jobs/" + created.JobID + "/events")
res, err := c.http.Get(c.url + "/api/jobs/" + created.JobID + "/events")
if err != nil {
t.Fatal(err)
}
@@ -139,6 +147,12 @@ func TestLocalJobLifecycle(t *testing.T) {
}
snapshotURL := "/api/snapshots/" + *job.SnapshotID
other := c.stranger()
other.json("GET", "/api/jobs/"+created.JobID, "", 404, nil)
other.json("POST", "/api/jobs/"+created.JobID+"/files?path=evil.js", "x", 404, nil)
other.json("GET", snapshotURL+"/scene", "", 404, nil)
other.json("GET", snapshotURL+"/entities/1/source", "", 404, nil)
var summary map[string]any
c.json("GET", snapshotURL, "", 200, &summary)
if summary["fileCount"] != 2.0 || summary["references"] != 1.0 || summary["name"] != "demo" {
@@ -255,3 +269,20 @@ func must[T any](v T, err error) T {
}
return v
}
func TestSessionCookieFlags(t *testing.T) {
_, c := newTestServer(t)
req, _ := http.NewRequest("POST", c.url+"/api/jobs", strings.NewReader(`{"kind":"local"}`))
req.Header.Set("X-Forwarded-Proto", "https")
res, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
res.Body.Close()
cookie := res.Header.Get("Set-Cookie")
for _, want := range []string{sessionCookie + "=", "HttpOnly", "Secure", "SameSite=Lax", "Path=/"} {
if !strings.Contains(cookie, want) {
t.Errorf("Set-Cookie %q missing %q", cookie, want)
}
}
}