mirror of
https://github.com/n8n-io/n8n
synced 2026-04-21 15:47:20 +00:00
fix: Block concurring connection requests in computer use (no-changelog) (#28312)
This commit is contained in:
parent
e8360a497d
commit
b48aeef1f2
2 changed files with 79 additions and 1 deletions
|
|
@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue