Skip to main content
Glama
queue-robustness-security-fixes.test.ts11 kB
import { SSHConnectionManager } from "../src/ssh-connection-manager"; import { SSHConnectionConfig, CommandSource, } from "../src/types"; // Mock the ssh2 module to avoid real SSH connections in unit tests jest.mock('ssh2', () => { const mockClient = { on: jest.fn(), shell: jest.fn(), connect: jest.fn(), destroy: jest.fn() }; return { Client: jest.fn(() => mockClient) }; }); describe("Queue Robustness and Security Fixes", () => { let connectionManager: SSHConnectionManager; const mockConfig: SSHConnectionConfig = { name: "robustness-test-session", host: "localhost", username: "testuser", password: "testpass" }; beforeEach(() => { connectionManager = new SSHConnectionManager(); jest.clearAllMocks(); // Setup standard mock behavior for all tests const mockClient = require('ssh2').Client(); const mockChannel = { on: jest.fn(), write: jest.fn(), end: jest.fn(), removeListener: jest.fn(), stderr: { on: jest.fn() }, setWindow: jest.fn() }; // Setup mocks to simulate successful connection mockClient.on = jest.fn((event, callback) => { if (event === 'ready') { setTimeout(() => callback(), 0); } }); mockClient.shell = jest.fn((callback) => { callback(null, mockChannel); }); mockClient.connect = jest.fn(); // Setup channel to simulate shell ready with prompt and command execution let dataCallback: ((data: Buffer) => void) | undefined; mockChannel.on = jest.fn((event, callback) => { if (event === 'data') { dataCallback = callback; // Simulate initial prompt setTimeout(() => { if (dataCallback) { dataCallback(Buffer.from('user@localhost:~$ ')); } }, 10); } }); // Mock command execution to simulate real output mockChannel.write = jest.fn((command: string) => { // Simulate command echoing back and result after short delay setTimeout(() => { if (dataCallback) { const commandText = command.replace('\n', ''); // Echo the command and simulate output dataCallback(Buffer.from(`${commandText}\n${commandText} output\nuser@localhost:~$ `)); } }, 50); }); }); afterEach(() => { connectionManager.cleanup(); }); describe("Race Condition in Queue Processing", () => { it("should fail - demonstrates race condition in processCommandQueue method", async () => { // This test demonstrates the race condition issue await connectionManager.createConnection(mockConfig); const commandPromises: Promise<any>[] = []; // Launch multiple commands simultaneously to trigger race condition // Current implementation checks isCommandExecuting and queue length separately // This creates a window where multiple commands can think they should execute for (let i = 0; i < 10; i++) { const promise = connectionManager.executeCommand( mockConfig.name, `echo "command ${i}"`, { source: "claude" as CommandSource } ); commandPromises.push(promise); // Add tiny delay to increase chance of hitting race condition if (i % 2 === 0) { await new Promise(resolve => setTimeout(resolve, 1)); } } // With the race condition, commands might execute out of order or fail // We expect FIFO order: commands should complete in order 0, 1, 2, ... const results = await Promise.all(commandPromises); // This test will currently pass by luck in most cases, but the race condition exists // The fix needs to make queue processing atomic expect(results).toHaveLength(10); // The race condition is hard to reproduce consistently, but it exists // The test will serve as a regression test after fixing }, 30000); }); describe("Missing Queue Size Limits - DoS Vulnerability", () => { it("should fail - demonstrates unlimited queue growth leading to memory exhaustion", async () => { await connectionManager.createConnection(mockConfig); const commandPromises: Promise<any>[] = []; // Try to queue excessive commands that would consume too much memory // Current implementation has no MAX_QUEUE_SIZE limit const excessiveCommandCount = 1000; // This should be rejected after 100 commands for (let i = 0; i < excessiveCommandCount; i++) { const promise = connectionManager.executeCommand( mockConfig.name, `echo "spam command ${i}"`, { source: "claude" as CommandSource } ); commandPromises.push(promise); } // This test will likely pass (which is the problem!) // We want it to fail with proper queue size validation try { await Promise.all(commandPromises); // If we get here without error, the vulnerability exists // If no error thrown, the DoS vulnerability exists throw new Error("Expected queue size limit to be enforced, but unlimited queuing was allowed"); } catch (error) { // This is what we want after fixing - queue limit enforcement expect((error as Error).message).toMatch(/queue.*full|queue.*limit|too many commands/i); } }, 30000); it("should fail - demonstrates no validation when adding commands beyond reasonable limits", async () => { await connectionManager.createConnection(mockConfig); // Add commands one by one to test queue size validation const maxReasonableQueueSize = 100; // Fill queue up to reasonable limit const promises: Promise<any>[] = []; for (let i = 0; i < maxReasonableQueueSize; i++) { promises.push(connectionManager.executeCommand( mockConfig.name, `echo "queued ${i}"`, { source: "claude" as CommandSource } )); } // This should fail when trying to exceed reasonable queue size await expect( connectionManager.executeCommand( mockConfig.name, "echo 'this should be rejected'", { source: "claude" as CommandSource } ) ).rejects.toThrow(/queue.*full|queue.*limit|maximum.*exceeded/i); // Clean up pending promises await Promise.allSettled(promises); }, 30000); }); describe("Queue Cleanup on Session Disconnect", () => { it("should fail - demonstrates promise leaks when disconnecting session with queued commands", async () => { await connectionManager.createConnection(mockConfig); // Queue several commands that won't complete immediately const pendingPromises: Promise<any>[] = []; for (let i = 0; i < 5; i++) { const promise = connectionManager.executeCommand( mockConfig.name, `sleep 10 && echo "long running ${i}"`, // Commands that take time { source: "claude" as CommandSource } ); pendingPromises.push(promise); } // Give commands time to enter queue await new Promise(resolve => setTimeout(resolve, 100)); // Disconnect session while commands are queued await connectionManager.disconnectSession(mockConfig.name); // The pending promises should be rejected, not left hanging // Current implementation doesn't handle this properly const settledResults = await Promise.allSettled(pendingPromises); // All promises should be rejected due to session disconnect const rejectedCount = settledResults.filter(result => result.status === 'rejected').length; expect(rejectedCount).toBe(pendingPromises.length); // Check that rejection reasons are appropriate settledResults.forEach(result => { if (result.status === 'rejected') { expect(result.reason.message).toMatch(/session.*disconnected|connection.*closed|session.*terminated/i); } }); }, 30000); it("should fail - demonstrates no cleanup of command queue during disconnect", async () => { await connectionManager.createConnection(mockConfig); // Create commands but don't await them const queuedPromises: Promise<any>[] = []; for (let i = 0; i < 3; i++) { queuedPromises.push(connectionManager.executeCommand( mockConfig.name, `echo "queued command ${i}"`, { source: "claude" as CommandSource } )); } // Disconnect before commands complete await connectionManager.disconnectSession(mockConfig.name); // All queued commands should fail gracefully for (const promise of queuedPromises) { await expect(promise).rejects.toThrow(/session.*not found|connection.*closed|session.*disconnected/i); } }, 20000); }); describe("Command Staleness Validation (Bonus)", () => { it("should fail - demonstrates no age validation for queued commands", async () => { await connectionManager.createConnection(mockConfig); // Create a command and artificially age it const staleCommandPromise = connectionManager.executeCommand( mockConfig.name, "echo 'potentially stale command'", { source: "claude" as CommandSource } ); // In a real scenario, commands older than 5 minutes (300000ms) should be rejected // This test is hard to create without modifying the timestamp, so it's more of a design test // For now, just ensure the command completes normally const result = await staleCommandPromise; expect(result.stdout).toContain('potentially stale command'); // After implementation, we would test that commands with artificially old timestamps get rejected }, 15000); }); describe("Atomic Queue State Management", () => { it("should fail - demonstrates non-atomic queue state checks", async () => { await connectionManager.createConnection(mockConfig); const rapidFireCommands: Promise<any>[] = []; // Fire commands as rapidly as possible to increase chance of hitting race condition for (let i = 0; i < 20; i++) { rapidFireCommands.push(connectionManager.executeCommand( mockConfig.name, `echo "rapid fire ${i}"`, { source: "claude" as CommandSource } )); } const results = await Promise.all(rapidFireCommands); // All commands should complete successfully with atomic queue processing expect(results).toHaveLength(20); results.forEach((result, index) => { expect(result.stdout).toContain(`rapid fire ${index}`); }); // This test might pass by luck even with race conditions // But it will serve as a regression test after implementing atomic operations }, 30000); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/LightspeedDMS/ssh-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server