fix(security): encode session navigation parameters (#11232)

This commit is contained in:
Jasper
2026-08-19 15:31:50 +00:00
committed by GitHub
parent 35bd51338c
commit 01695e8bd9
2 changed files with 61 additions and 2 deletions
@@ -0,0 +1,54 @@
import { act, renderHook } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
const navigateMock = vi.hoisted(() => vi.fn());
const routerState = vi.hoisted(() => ({ searchParams: new URLSearchParams() }));
vi.mock('react-router', () => ({
useNavigate: () => navigateMock,
useLocation: () => ({ pathname: '/pair', search: routerState.searchParams.toString() }),
useSearchParams: () => [routerState.searchParams, vi.fn()],
}));
vi.mock('../acp/sessions', () => ({
acpGetSessionListItem: vi.fn(() => new Promise(() => {})),
acpListRecentSessions: vi.fn().mockResolvedValue([]),
}));
vi.mock('../contexts/ChatContext', () => ({
useChatContext: () => ({ chat: { sessionId: undefined } }),
}));
import { useNavigationSessions } from './useNavigationSessions';
const injectedSessionId = 'session-1&shouldStartAgent=true';
function expectSingleSessionParameter(target: string) {
const url = new URL(target, 'http://localhost');
expect(url.searchParams.get('resumeSessionId')).toBe(injectedSessionId);
expect(url.searchParams.get('shouldStartAgent')).toBeNull();
}
describe('useNavigationSessions', () => {
beforeEach(() => {
navigateMock.mockReset();
routerState.searchParams = new URLSearchParams();
});
it('keeps a selected session ID in one query parameter', () => {
const { result } = renderHook(() => useNavigationSessions());
act(() => result.current.handleSessionClick(injectedSessionId));
expectSingleSessionParameter(navigateMock.mock.calls[0][0]);
});
it('keeps a retained session ID in one query parameter', () => {
routerState.searchParams = new URLSearchParams({ resumeSessionId: injectedSessionId });
const { result } = renderHook(() => useNavigationSessions());
act(() => result.current.handleNavClick('/pair'));
expectSingleSessionParameter(navigateMock.mock.calls[0][0]);
});
});
@@ -13,6 +13,11 @@ import { groupSessionsByProject } from '../utils/projectSessions';
const MAX_RECENT_SESSIONS = 25;
function pairSessionPath(sessionId: string): string {
const searchParams = new URLSearchParams({ resumeSessionId: sessionId });
return `/pair?${searchParams.toString()}`;
}
export function prependUnique(
prev: SessionListItem[],
session: SessionListItem
@@ -188,7 +193,7 @@ export function useNavigationSessions() {
const sessionId =
currentSessionId || lastSessionIdRef.current || chatContext?.chat?.sessionId;
if (sessionId && sessionId.length > 0) {
navigate(`/pair?resumeSessionId=${sessionId}`);
navigate(pairSessionPath(sessionId));
} else {
navigate('/');
}
@@ -201,7 +206,7 @@ export function useNavigationSessions() {
const handleSessionClick = useCallback(
(sessionId: string) => {
navigate(`/pair?resumeSessionId=${sessionId}`);
navigate(pairSessionPath(sessionId));
},
[navigate]
);