fix: Block concurring connection requests in computer use (no-changelog) (#28312)

This commit is contained in:
Dimitri Lavrenük 2026-04-14 09:29:25 +02:00 committed by GitHub
parent e8360a497d
commit b48aeef1f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 79 additions and 1 deletions

View file

@ -326,6 +326,70 @@ describe('POST /connect — origin allowlist', () => {
});
});
// ---------------------------------------------------------------------------
// POST /connect — concurrent confirmation
// ---------------------------------------------------------------------------
describe('POST /connect — concurrent confirmation', () => {
it('returns 409 when a confirmation prompt is already in progress', async () => {
let resolveConfirm!: (value: boolean) => void;
const confirmConnect = jest
.fn()
.mockImplementation(
async () => await new Promise<boolean>((resolve) => (resolveConfirm = resolve)),
);
const { port, close } = await startTestDaemon(
{ filesystem: { dir: tmpDir } },
{ confirmConnect },
);
try {
// First connection — hangs waiting for user confirmation
const first = post(port, '/connect', { url: 'http://localhost:5678', token: 'tok' });
// Wait for the first request to reach confirmConnect
await new Promise((resolve) => setTimeout(resolve, 50));
// Second connection attempt while confirmation is pending
const second = await post(port, '/connect', { url: 'http://localhost:5679', token: 'tok' });
expect(second.status).toBe(409);
expect(second.body.error).toMatch(/confirmation is already in progress/);
// Resolve the first confirmation and await its response
resolveConfirm(false);
await first;
} finally {
await close();
}
});
it('accepts a new connection after a pending confirmation completes', async () => {
let resolveConfirm!: (value: boolean) => void;
const confirmConnect = jest
.fn()
.mockImplementationOnce(
async () => await new Promise<boolean>((resolve) => (resolveConfirm = resolve)),
)
.mockResolvedValue(true);
const { port, close } = await startTestDaemon(
{ filesystem: { dir: tmpDir } },
{ confirmConnect },
);
try {
// First connection — hangs then gets rejected
const first = post(port, '/connect', { url: 'http://localhost:5678', token: 'tok' });
await new Promise((resolve) => setTimeout(resolve, 50));
resolveConfirm(false);
await first;
// Second connection after confirmation cleared — should succeed
const second = await post(port, '/connect', { url: 'http://localhost:5678', token: 'tok' });
expect(second.status).toBe(200);
} finally {
await close();
}
});
});
// ---------------------------------------------------------------------------
// POST /connect — already connected
// ---------------------------------------------------------------------------

View file

@ -43,6 +43,7 @@ interface DaemonState {
session: GatewaySession | null;
connectedAt: string | null;
connectedUrl: string | null;
confirmingConnection: boolean;
}
const state: DaemonState = {
@ -51,6 +52,7 @@ const state: DaemonState = {
session: null,
connectedAt: null,
connectedUrl: null,
confirmingConnection: false,
};
// ---------------------------------------------------------------------------
@ -180,6 +182,12 @@ async function handleConnect(req: http.IncomingMessage, res: http.ServerResponse
return;
}
// Reject concurrent connection attempts while a confirmation prompt is active
if (state.confirmingConnection) {
jsonResponse(req, res, 409, { error: 'A connection confirmation is already in progress.' });
return;
}
let parsedOrigin: string;
try {
parsedOrigin = new URL(url).origin;
@ -206,7 +214,13 @@ async function handleConnect(req: http.IncomingMessage, res: http.ServerResponse
const defaults = store.getDefaults(state.config);
const session = new GatewaySession(defaults, store);
const approved = await daemonOptions.confirmConnect(url, session);
state.confirmingConnection = true;
let approved: boolean;
try {
approved = await daemonOptions.confirmConnect(url, session);
} finally {
state.confirmingConnection = false;
}
if (!approved) {
jsonResponse(req, res, 403, { error: 'Connection rejected by user.' });
return;