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

0:01.4The 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…
0:02.0The 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
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…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.