All bugs
highHidden state corruption

Members list sends every pending invite's secret token to the browser

The Members page looks right: one member, one pending invite. The response behind it isn't. It carries the token that accepts the invite, and the page only ever uses it as a React key.

electric-forestFound 2026-10-27Full report in Replay QA

What the recording showed

GET /api/orgs/qa-exploration-workspace/members returned 200 with invites[0].token in the body. MembersPage reads s.token only as a list key; invite links come from the link field of POST /invites.

Actual

GET /api/orgs/qa-exploration-workspace/members returns HTTP 200 with an application/json body containing invites:[{ "token":"Tq7Xk2Vb9MwR4nLz8PcYd3HsGf6JaE1u", ... }] — the raw invite token is sent to the browser. The client page only uses that token as a React list key and never for any legitimate client-facing purpose.

Expected

The members listing response should not expose invite tokens to the browser. The client needs only the invite's display fields (email, role, status, invitedAt); the invite token is a server-side credential used to accept the invite and should be omitted (or redacted) from GET /api/orgs/{org}/members so it is never delivered to the client.

Session replay

Chronology of events

1
The app at 0:01.40:01.4
0:01.4

The user navigates to the members route of workspace qa-exploration-workspace (the frame shows the 'Loading route…' state); this triggers the client's fetch of the members endpoint (XGe in index-CPiqmCDG.js line 98640: fetch('/api/orgs/.../members')).

1 evidence probe (NetworkRequest)
NetworkRequest {"mode":"detail","requestId":"3006.18"}
→ A GET network request with ID 3006.18 was made to https://electric-forest-production.up.railway.app/api/orgs/qa-exploration-workspace/members:
<request_summary>
The response had status 200 ().
</request_summary>
Initiated at Point:111 @ 1438.00ms
Events:
  request @ 1438ms
  response @ 1686ms
  response-body @ 1694ms
  request-done @ 1694ms
Request headers:
<requestHeaders>
sec-ch-ua: 
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/108.0.5359.0 Safari/537.36
sec-ch-ua-platform: 
</requestHeaders>
Response headers:
<responseHeaders>
date: Tue, 27 Oct 2026 13:15:07 GMT
content-encoding: gzip
referrer-policy: same-origin
x-content-type-options: nosniff
server: railway-hikari
vary: accept-encoding
content-type: application/json
x-railway-edge: jfk1
x-hikari-trace: jfk1.pqzh
x-railway-request-id: XFXSSbgFT9-jwxNWwUFZXw
</responseHeaders>

The request body was
 EMPTY.

The response body was
:
<responseBody>
{"org":"qa-exploration-workspace","owner":"auth0|6ae093eadb6f58d9b662665b","me":{"sub":"auth0|6ae093eadb6f58d9b662665b","role":"owner"},"humans":[{"sub":"auth0|6ae093eadb6f58d9b662665b","email":"qa-proj-electric-forest-production-up-railway-app-mvqn0vn3@reloulexei.resend.app","role":"owner","status":"active"}],"invites":[{"token":"Tq7Xk2Vb9MwR4nLz8PcYd3HsGf6JaE1u","email":"qa-proj-electric-forest-production-up-railway-app-mvqn0vn3@reloulexei.resend.app","role":"member","invitedBy":"auth0|6ae093eadb6f58d9b662665b","invitedAt":1793…
2
The app at 0:02.00:02.0
0:02.0

The Members page for workspace qa-exploration-workspace loads and the client fetches GET /api/orgs/qa-exploration-workspace/members; the 200 response body includes the pending invite entry with its raw token credential (value Tq7Xk2Vb9MwR4nLz8PcYd3HsGf6JaE1u), which is exposed to the browser. The page renders the Humans/Members and Invited lists.

1 evidence probe (NetworkRequest)
NetworkRequest {"mode":"detail","requestId":"3006.18"}
→ A GET network request with ID 3006.18 was made to https://electric-forest-production.up.railway.app/api/orgs/qa-exploration-workspace/members:
<request_summary>
The response had status 200 ().
</request_summary>
Initiated at Point:111 @ 1438.00ms
Events:
  request @ 1438ms
  response @ 1686ms
  response-body @ 1694ms
  request-done @ 1694ms
Request headers:
<requestHeaders>
sec-ch-ua: 
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/108.0.5359.0 Safari/537.36
sec-ch-ua-platform: 
</requestHeaders>
Response headers:
<responseHeaders>
date: Tue, 27 Oct 2026 13:15:07 GMT
content-encoding: gzip
referrer-policy: same-origin
x-content-type-options: nosniff
server: railway-hikari
vary: accept-encoding
content-type: application/json
x-railway-edge: jfk1
x-hikari-trace: jfk1.pqzh
x-railway-request-id: XFXSSbgFT9-jwxNWwUFZXw
</responseHeaders>

The request body was
 EMPTY.

The response body was
:
<responseBody>
{"org":"qa-exploration-workspace","owner":"auth0|6ae093eadb6f58d9b662665b","me":{"sub":"auth0|6ae093eadb6f58d9b662665b","role":"owner"},"humans":[{"sub":"auth0|6ae093eadb6f58d9b662665b","email":"qa-proj-electric-forest-production-up-railway-app-mvqn0vn3@reloulexei.resend.app","role":"owner","status":"active"}],"invites":[{"token":"Tq7Xk2Vb9MwR4nLz8PcYd3HsGf6JaE1u","email":"qa-proj-electric-forest-production-up-railway-app-mvqn0vn3@reloulexei.resend.app","role":"member","invitedBy":"auth0|6ae093eadb6f58d9b662665b","invitedAt":1793…

Root cause

The server-side members endpoint serializes the full invite record into the response, including the invite-acceptance credential: the response body of GET /api/orgs/qa-exploration-workspace/members contains "invites":[{"token":"Tq7Xk2Vb9MwR4nLz8PcYd3HsGf6JaE1u",...}]. The client never needs this value — MembersPage-BEGlMEYZ.js line 596 only references s.token as the React list key (children are rendered from s.email), and invite links are obtained separately from the POST /invites response's link field (index-CPiqmCDG.js line 98672 f.link). Since the field is present in the server's response to the browser and is not a legitimately client-facing token, the server is leaking the invite credential to the client.

3 evidence probes (NetworkRequest, ReadSource)
NetworkRequest {"mode":"detail","requestId":"3006.18"}
→ A GET network request with ID 3006.18 was made to https://electric-forest-production.up.railway.app/api/orgs/qa-exploration-workspace/members:
<request_summary>
The response had status 200 ().
</request_summary>
Initiated at Point:111 @ 1438.00ms
Events:
  request @ 1438ms
  response @ 1686ms
  response-body @ 1694ms
  request-done @ 1694ms
Request headers:
<requestHeaders>
sec-ch-ua: 
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/108.0.5359.0 Safari/537.36
sec-ch-ua-platform: 
</requestHeaders>
Response headers:
<responseHeaders>
date: Tue, 27 Oct 2026 13:15:07 GMT
content-encoding: gzip
referrer-policy: same-origin
x-content-type-options: nosniff
server: railway-hikari
vary: accept-encoding
content-type: application/json
x-railway-edge: jfk1
x-hikari-trace: jfk1.pqzh
x-railway-request-id: XFXSSbgFT9-jwxNWwUFZXw
</responseHeaders>

The request body was
 EMPTY.

The response body was
:
<responseBody>
{"org":"qa-exploration-workspace","owner":"auth0|6ae093eadb6f58d9b662665b","me":{"sub":"auth0|6ae093eadb6f58d9b662665b","role":"owner"},"humans":[{"sub":"auth0|6ae093eadb6f58d9b662665b","email":"qa-proj-electric-forest-production-up-railway-app-mvqn0vn3@reloulexei.resend.app","role":"owner","status":"active"}],"invites":[{"token":"Tq7Xk2Vb9MwR4nLz8PcYd3HsGf6JaE1u","email":"qa-proj-electric-forest-production-up-railway-app-mvqn0vn3@reloulexei.resend.app","role":"member","invitedBy":"auth0|6ae093eadb6f58d9b662665b","invitedAt":1793…
ReadSource {"line":596,"path":"https://electric-forest-production.up.railway.app/assets/MembersPage-BEGlMEYZ.js"}
→  hits | L | code 
------|------|------
     |  571 |                           badge: s.role, 
   0 |  572 |                           children: s.email??I(s.sub) 
     |  573 |                         }, 
     |  574 |                         s.sub 
     |  575 |                       ) 
     |  576 |                     ) 
     |  577 |                   } 
     |  578 |                 ), 
     |  579 |                 b.length === 0 
     |  580 |                 ? null 
 157 |  581 |                 : e.jsx( 
     |  582 |                   k, 
     |  583 |                   { 
     |  584 |                     label: "Invited · awaiting sign-in", 
     |  585 |                     count: b.length, 
 157 |  586 |                     children: b.map( 
 157 |  587 |                       s => e.jsx( 
     |  588 |                         M, 
     |  589 |                         { 
     |  590 |                           "data-testid": "members-invite", 
     |  591 |                           user: { name: s.email, c: "#555b58", role: "#a9adaa" }, 
     |  592 |                           status: "offline", 
     |  593 |                           badge: `${s.role} · pending`, 
     |  594 |                           children: s.email 
     |  595 |                         }, 
     | *596 |                         s.token 
     |  597 |                       ) 
     |  598 |                     ) 
     |  599 |                   } 
     |  600 |                 ) 
     | …
ReadSource {"line":98672,"path":"https://electric-forest-production.up.railway.app/assets/index-CPiqmCDG.js"}
→  hits | L | code 
------|------|------
     | 98625 |         ...ju, 
     | 98626 |         reconnectDelayMs: 1e3 
     | 98627 |       } 
     | 98628 |     ), 
   0 | 98629 |     i = y0(n, { replayedOffset: r.checkpoint }), 
   0 | 98630 |     o = Lo( 
   0 | 98631 |       { apiPath: `/api/chat/${Mv(e)}`, streamId: Ene(e), reducerId: q6, follow: !1 } 
     | 98632 |     ).state.channels[t]?.topic??""; 
   0 | 98633 |   return { streamId: n, projection: r, dispatch: i, topic: o } 
     | 98634 | } 
     | 98635 | function XGe(e) { 
 158 | 98636 |   const[t, n] = E.useState(void 0), 
 158 | 98637 |     [r, i] = E.useState("loading"), 
 158 | 98638 |     s = E.useCallback(async() => { 
     | 98639 |       try { 
   1 | 98640 |         const a = await fetch( 
   1 | 98641 |           `/api/orgs/${encodeURIComponent(e)}/members`, 
     | 98642 |           { credentials: "same-origin" } 
     | 98643 |         ); 
   1 | 98644 |         if (!a.ok) { 
   0 | 98645 |           i(`error:${String(a.status)}`); 
   0 | 98646 |           return 
     | 98647 |         } 
   1 | 98648 |         n(await a.json()), i("ready") 
     | 98649 |       } catch (a) { 
   0 | 98650 |         i(`error:${a instanceof Error ? a.message : "unreachable"}`) 
     | 98651 |       } 
     | 98652 |     }, [e]); 
 158 | 98653 |   E.useEffect(() => { 
   1 | 98654 |     s() 
     | 98655 |   }, [s]); 
 158 | 98656 |   const o = E.useCallback(async(a, l) => { 
   0 | 98657 |       const u = await fetch( …

Root cause → actual behavior

1

The server serializes the pending invite record (including its secret token) into the GET /members response body, which is delivered to the browser over the network with content-type application/json.

1 evidence probe (NetworkRequest)
NetworkRequest {"mode":"detail","requestId":"3006.18"}
→ A GET network request with ID 3006.18 was made to https://electric-forest-production.up.railway.app/api/orgs/qa-exploration-workspace/members:
<request_summary>
The response had status 200 ().
</request_summary>
Initiated at Point:111 @ 1438.00ms
Events:
  request @ 1438ms
  response @ 1686ms
  response-body @ 1694ms
  request-done @ 1694ms
Request headers:
<requestHeaders>
sec-ch-ua: 
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/108.0.5359.0 Safari/537.36
sec-ch-ua-platform: 
</requestHeaders>
Response headers:
<responseHeaders>
date: Tue, 27 Oct 2026 13:15:07 GMT
content-encoding: gzip
referrer-policy: same-origin
x-content-type-options: nosniff
server: railway-hikari
vary: accept-encoding
content-type: application/json
x-railway-edge: jfk1
x-hikari-trace: jfk1.pqzh
x-railway-request-id: XFXSSbgFT9-jwxNWwUFZXw
</responseHeaders>

The request body was
 EMPTY.

The response body was
:
<responseBody>
{"org":"qa-exploration-workspace","owner":"auth0|6ae093eadb6f58d9b662665b","me":{"sub":"auth0|6ae093eadb6f58d9b662665b","role":"owner"},"humans":[{"sub":"auth0|6ae093eadb6f58d9b662665b","email":"qa-proj-electric-forest-production-up-railway-app-mvqn0vn3@reloulexei.resend.app","role":"owner","status":"active"}],"invites":[{"token":"Tq7Xk2Vb9MwR4nLz8PcYd3HsGf6JaE1u","email":"qa-proj-electric-forest-production-up-railway-app-mvqn0vn3@reloulexei.resend.app","role":"member","invitedBy":"auth0|6ae093eadb6f58d9b662665b","invitedAt":1793…
2

The client-side MembersPage code consumes this snapshot but only uses s.token as a React list key (line 596) — the token value is not required for rendering the invite list, so the credential-typed field is exposed to the browser without any legitimate need.

1 evidence probe (ReadSource)
ReadSource {"line":596,"path":"https://electric-forest-production.up.railway.app/assets/MembersPage-BEGlMEYZ.js"}
→  hits | L | code 
------|------|------
     |  571 |                           badge: s.role, 
   0 |  572 |                           children: s.email??I(s.sub) 
     |  573 |                         }, 
     |  574 |                         s.sub 
     |  575 |                       ) 
     |  576 |                     ) 
     |  577 |                   } 
     |  578 |                 ), 
     |  579 |                 b.length === 0 
     |  580 |                 ? null 
 157 |  581 |                 : e.jsx( 
     |  582 |                   k, 
     |  583 |                   { 
     |  584 |                     label: "Invited · awaiting sign-in", 
     |  585 |                     count: b.length, 
 157 |  586 |                     children: b.map( 
 157 |  587 |                       s => e.jsx( 
     |  588 |                         M, 
     |  589 |                         { 
     |  590 |                           "data-testid": "members-invite", 
     |  591 |                           user: { name: s.email, c: "#555b58", role: "#a9adaa" }, 
     |  592 |                           status: "offline", 
     |  593 |                           badge: `${s.role} · pending`, 
     |  594 |                           children: s.email 
     |  595 |                         }, 
     | *596 |                         s.token 
     |  597 |                       ) 
     |  598 |                     ) 
     |  599 |                   } 
     |  600 |                 ) 
     | …

Is your app hiding bugs like this?

Replay QA explores every build, records what it finds, and writes the root cause.