From 596d7c0a0b97e1f362e9461c5311c4905a5230f2 Mon Sep 17 00:00:00 2001 From: Johan Lindh Date: Fri, 25 Sep 2026 20:13:48 +0200 Subject: [PATCH 1/3] Derive AutoSession cookie security from page request --- jaws.go | 8 +++++-- request.go | 3 ++- request_test.go | 57 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/jaws.go b/jaws.go index 694d6712..948342f6 100644 --- a/jaws.go +++ b/jaws.go @@ -94,8 +94,12 @@ type Jaws struct { // It defaults to [assets.DefaultCookieName], which is derived from the // executable and falls back to "jaws". CookieName must be a valid, non-empty // HTTP cookie name; see [http.Cookie.Valid]. - CookieName string - AutoSession bool // Create a session during a successful WebSocket upgrade when a Request has none and the Session limits allow it. Defaults to false. + CookieName string + // AutoSession creates a [Session] on a successful WebSocket upgrade. + // + // It applies when the [Request] has none and Session limits allow it. The + // cookie's Secure flag follows the initial page request's scheme. Defaults to false. + AutoSession bool // TrustForwardedHeaders enables trusted proxy header processing. // // It governs the session cookie Secure flag and WebSocket Origin scheme diff --git a/request.go b/request.go index d950613c..aea74d2e 100644 --- a/request.go +++ b/request.go @@ -372,7 +372,8 @@ func (rq *Request) ensureAutoSession(w http.ResponseWriter, r *http.Request) { // through Jaws session lookups. func (rq *Request) newAutoSession(r *http.Request) (sess *Session) { jw := rq.Jaws - secure := secureheaders.RequestIsSecure(r, jw.TrustForwardedHeaders) + // The bundled client uses the page scheme, which Origin validation already checked. + secure := secureheaders.RequestIsSecure(rq.Initial(), jw.TrustForwardedHeaders) remoteIP := jw.clientIP(r) jw.mu.Lock() defer jw.mu.Unlock() diff --git a/request_test.go b/request_test.go index 616b0613..02c277b0 100644 --- a/request_test.go +++ b/request_test.go @@ -4151,6 +4151,63 @@ func TestWS_AutoSessionCreatesSession(t *testing.T) { } } +func TestWS_AutoSessionSecureFollowsPage(t *testing.T) { + for _, tc := range []struct { + name string + pageScheme string + upgradeScheme string + wantSecure bool + }{ + {"HTTP page", "http", "", false}, + {"HTTPS scheme on both requests", "https", "https", true}, + {"HTTPS scheme only on page", "https", "", true}, + } { + t.Run(tc.name, func(t *testing.T) { + jw, err := New() + if err != nil { + t.Fatal(err) + } + jw.AutoSession = true + jw.TrustForwardedHeaders = true + go jw.Serve() + t.Cleanup(jw.Close) + server := httptest.NewServer(jw) + t.Cleanup(server.Close) + u, err := url.Parse(server.URL) + if err != nil { + t.Fatal(err) + } + page := httptest.NewRequest(http.MethodGet, server.URL+"/", nil) + page.Header.Set("X-Forwarded-For", "203.0.113.9") + page.Header.Set("X-Forwarded-Proto", tc.pageScheme) + rq := jw.NewRequest(httptest.NewRecorder(), page) + header := http.Header{} + header.Set("Origin", tc.pageScheme+"://"+u.Host) + header.Set("X-Forwarded-For", "203.0.113.9") + if tc.upgradeScheme != "" { + header.Set("X-Forwarded-Proto", tc.upgradeScheme) + } + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + conn, resp, err := websocket.Dial(ctx, server.URL+"/jaws/"+rq.JawsKeyString(), &websocket.DialOptions{HTTPHeader: header}) + if err != nil { + t.Fatal(err) + } + defer func() { _ = conn.CloseNow() }() + if resp.StatusCode != http.StatusSwitchingProtocols { + t.Fatalf("status = %d, want 101", resp.StatusCode) + } + cookies := resp.Cookies() + if len(cookies) != 1 { + t.Fatalf("AutoSession cookies = %v, want one", cookies) + } + if cookies[0].Secure != tc.wantSecure { + t.Fatalf("AutoSession cookie Secure = %v, want %v", cookies[0].Secure, tc.wantSecure) + } + }) + } +} + func TestWS_AutoSessionDoesNotCreateAfterJawsClose(t *testing.T) { jw, err := New() if err != nil { From 458ec57e6ea011b4e573132333caf2a062545194 Mon Sep 17 00:00:00 2001 From: Johan Lindh Date: Fri, 25 Sep 2026 20:20:15 +0200 Subject: [PATCH 2/3] test: cover upgrade-only HTTPS scheme for AutoSession --- request_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/request_test.go b/request_test.go index 02c277b0..3eca0484 100644 --- a/request_test.go +++ b/request_test.go @@ -4161,6 +4161,7 @@ func TestWS_AutoSessionSecureFollowsPage(t *testing.T) { {"HTTP page", "http", "", false}, {"HTTPS scheme on both requests", "https", "https", true}, {"HTTPS scheme only on page", "https", "", true}, + {"HTTPS scheme only on upgrade", "http", "https", false}, } { t.Run(tc.name, func(t *testing.T) { jw, err := New() From 7eeec87ef3e4986f10c14c253d33880afe83f4c3 Mon Sep 17 00:00:00 2001 From: Johan Lindh Date: Fri, 25 Sep 2026 20:22:44 +0200 Subject: [PATCH 3/3] docs: clarify AutoSession cookie security --- jaws.go | 5 +++-- request.go | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/jaws.go b/jaws.go index 948342f6..d685bea2 100644 --- a/jaws.go +++ b/jaws.go @@ -97,8 +97,9 @@ type Jaws struct { CookieName string // AutoSession creates a [Session] on a successful WebSocket upgrade. // - // It applies when the [Request] has none and Session limits allow it. The - // cookie's Secure flag follows the initial page request's scheme. Defaults to false. + // It applies when the [Request] has no Session and Session limits allow one. + // The cookie's Secure flag follows the initial page request's scheme, honoring + // [Jaws.TrustForwardedHeaders]. AutoSession defaults to false. AutoSession bool // TrustForwardedHeaders enables trusted proxy header processing. // diff --git a/request.go b/request.go index aea74d2e..fe17094b 100644 --- a/request.go +++ b/request.go @@ -372,7 +372,6 @@ func (rq *Request) ensureAutoSession(w http.ResponseWriter, r *http.Request) { // through Jaws session lookups. func (rq *Request) newAutoSession(r *http.Request) (sess *Session) { jw := rq.Jaws - // The bundled client uses the page scheme, which Origin validation already checked. secure := secureheaders.RequestIsSecure(rq.Initial(), jw.TrustForwardedHeaders) remoteIP := jw.clientIP(r) jw.mu.Lock()