fix: validate session token update events (#2018)

Co-authored-by: LawyerLyu <georgelyutwitter@gmail.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
吕盈辉律师
2026-05-17 18:46:15 -07:00
committed by GitHub
co-authored by LawyerLyu Steven Enamakel
parent 0257b2e662
commit db087a7d3e
2 changed files with 108 additions and 1 deletions
+26 -1
View File
@@ -75,6 +75,31 @@ export function coreStatePollFailureWarningMessage(failureCount: number): string
return null;
}
function decodeJwtPayload(token: string): Record<string, unknown> | null {
const [, payload] = token.split('.');
if (!payload) return null;
try {
const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');
const decoded = window.atob(padded);
return JSON.parse(decoded) as Record<string, unknown>;
} catch {
return null;
}
}
function isPlausibleSessionToken(token: unknown): token is string {
if (typeof token !== 'string') return false;
if (token.trim() !== token || token.length === 0) return false;
if (token.split('.').length !== 3) return false;
const payload = decodeJwtPayload(token);
if (!payload || typeof payload.exp !== 'number') return false;
return payload.exp * 1000 > Date.now();
}
interface CoreStateContextValue extends CoreState {
refresh: () => Promise<void>;
refreshTeams: () => Promise<void>;
@@ -458,7 +483,7 @@ export default function CoreStateProvider({ children }: { children: ReactNode })
const onSessionTokenUpdated = (event: Event) => {
const customEvent = event as CustomEvent<{ sessionToken?: string | null }>;
const token = customEvent.detail?.sessionToken;
if (typeof token !== 'string' || token.length === 0) {
if (!isPlausibleSessionToken(token)) {
return;
}
@@ -45,6 +45,13 @@ function makeSnapshot(overrides: {
};
}
function makeJwt(payload: Record<string, unknown>): string {
const encode = (value: Record<string, unknown>) =>
window.btoa(JSON.stringify(value)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
return `${encode({ alg: 'none', typ: 'JWT' })}.${encode(payload)}.signature`;
}
type CoreStateContextValue = ReturnType<typeof useCoreState>;
function Consumer({ captureCtx }: { captureCtx?: (ctx: CoreStateContextValue) => void }) {
@@ -297,6 +304,81 @@ describe('CoreStateProvider — identity-change cache clearing', () => {
);
});
it('ignores malformed session-token-updated events (#1937)', async () => {
fetchSnapshot.mockResolvedValue(makeSnapshot({ userId: null, sessionToken: null }));
listTeams.mockResolvedValue([]);
render(
<CoreStateProvider>
<Consumer />
</CoreStateProvider>
);
await waitFor(() => expect(screen.getByTestId('ready').textContent).toBe('ready'));
await act(async () => {
window.dispatchEvent(
new CustomEvent('core-state:session-token-updated', {
detail: { sessionToken: 'not-a-jwt' },
})
);
});
expect(screen.getByTestId('token').textContent).toBe('none');
expect(fetchSnapshot).toHaveBeenCalledTimes(1);
});
it('ignores expired JWT-shaped session-token-updated events (#1937)', async () => {
const expiredToken = makeJwt({ exp: Math.floor(Date.now() / 1000) - 60 });
fetchSnapshot.mockResolvedValue(makeSnapshot({ userId: null, sessionToken: null }));
listTeams.mockResolvedValue([]);
render(
<CoreStateProvider>
<Consumer />
</CoreStateProvider>
);
await waitFor(() => expect(screen.getByTestId('ready').textContent).toBe('ready'));
await act(async () => {
window.dispatchEvent(
new CustomEvent('core-state:session-token-updated', {
detail: { sessionToken: expiredToken },
})
);
});
expect(screen.getByTestId('token').textContent).toBe('none');
expect(fetchSnapshot).toHaveBeenCalledTimes(1);
});
it('accepts unexpired JWT-shaped session-token-updated events (#1937)', async () => {
const token = makeJwt({ exp: Math.floor(Date.now() / 1000) + 60 });
fetchSnapshot
.mockResolvedValueOnce(makeSnapshot({ userId: null, sessionToken: null }))
.mockResolvedValueOnce(
makeSnapshot({ userId: null, sessionToken: token, isAuthenticated: true })
);
listTeams.mockResolvedValue([]);
render(
<CoreStateProvider>
<Consumer />
</CoreStateProvider>
);
await waitFor(() => expect(screen.getByTestId('ready').textContent).toBe('ready'));
await act(async () => {
window.dispatchEvent(
new CustomEvent('core-state:session-token-updated', { detail: { sessionToken: token } })
);
});
expect(screen.getByTestId('token').textContent).toBe(token);
});
it('setMeetAutoOrchestratorHandoff(true) calls update RPC + flips snapshot optimistically (#1299)', async () => {
fetchSnapshot.mockResolvedValue(makeSnapshot({ userId: 'u1', sessionToken: 'tok1' }));
listTeams.mockResolvedValue([]);