Security Fix: Consistent httpOnly Cookies on Invite Acceptance
Security Fix: Consistent httpOnly Cookies on Invite Acceptance
Version: 1.0.29
Overview
This release closes a subtle but important security inconsistency in how the platform sets the active organisation cookie during invite acceptance. The invite acceptance page now delegates cookie creation to the server, ensuring all organisation-context cookies are issued as httpOnly — consistent with every other part of the application.
Background: Two Cookie Paths
The platform tracks which gym (organisation) a user is currently operating in via a cookie. There are two places this cookie can be set:
| Path | Mechanism | Cookie type |
|---|---|---|
| Standard gym switch | POST /api/auth/set-active-gym | httpOnly (server-set) |
| Invite acceptance (before this fix) | document.cookie = ... | Non-httpOnly (client-set) |
Why Does This Matter?
An httpOnly cookie cannot be read by JavaScript running in the browser. This is a standard defence against Cross-Site Scripting (XSS) attacks: even if an attacker manages to inject malicious JavaScript into the page, they cannot read or steal an httpOnly cookie.
A non-httpOnly cookie, by contrast, is fully accessible to any JavaScript on the page — including injected scripts. For a session or context cookie, this is an unnecessary exposure.
Before this fix, a user who joined via an invite link would have their activeOrg cookie set client-side, creating a window where XSS could read the organisation identifier. All subsequent navigation (including login, gym switching, and dashboard loads) used the more secure server-issued httpOnly cookie, making the invite flow the odd one out.
What Changed
The invite acceptance success handler in src/app/invite/[token]/page.tsx has been updated to call the existing /api/auth/set-active-gym API route instead of writing the cookie directly.
Before
// src/app/invite/[token]/page.tsx (old)
document.cookie = `activeOrg=${result.orgId}; path=/`;
router.push('/dashboard');
After
// src/app/invite/[token]/page.tsx (new)
await fetch('/api/auth/set-active-gym', {
method: 'POST',
body: JSON.stringify({ gymId: result.orgId }),
headers: { 'Content-Type': 'application/json' },
});
router.push('/dashboard');
The /api/auth/set-active-gym route (line 56 of src/app/api/auth/set-active-gym/route.ts) issues the cookie with the httpOnly flag via the Set-Cookie response header, so JavaScript never has direct access to its value.
Impact
- Security: The
activeOrgcookie is now consistentlyhttpOnlyregardless of how a user first joins an organisation. XSS attacks can no longer read this cookie from the invite flow. - Functionality: No change. The tRPC context reads the cookie from the request headers (server-side) in all cases, so both old and new formats worked functionally. The redirect to
/dashboardafter invite acceptance is unchanged. - Existing sessions: Users who accepted an invite before this fix and still have the old non-
httpOnlycookie will naturally rotate to the secure version the next time they switch gyms or log in.
No Action Required
This change is transparent to end users and platform administrators. No configuration changes, database migrations, or redeployment steps beyond the standard release process are needed.