Skip to main content
Glama
ssh-key-security-validation.test.ts12.2 kB
/** * TDD Tests for SSH Key File Security and Validation Fixes * Based on code reviewer feedback for path traversal, input validation, * error sanitization, and async consistency issues */ import { SSHConnectionManager } from "../src/ssh-connection-manager.js"; import { SSHConnectionConfig } from "../src/types.js"; import * as fs from "fs/promises"; import * as fsSync from "fs"; import * as os from "os"; import * as path from "path"; describe("SSH Key File Security and Validation", () => { let connectionManager: SSHConnectionManager; const testKeyDir = path.join(os.tmpdir(), "ssh-key-security-test"); // Valid test key for legitimate tests const validTestKey = `-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEA4f6wHWw1mKtP7Vm7rF8vWmEBGkI2vNKPfZ2bE3Sd8Q9Hb2yH YNMqCmKlV5l5Q3aZ9z7K3D3k1J7W2q9B9v4A8F7H3E1QQ6oV5J2b3c4d5E6F7G8H Test RSA key - FOR TESTING ONLY - NOT A REAL KEY 9I0J1K2L3M4N5O6P7Q8R9S0T1U2V3W4X5Y6Z7A8B9C0D1E2F3G4H5I6J7K8L9M0N 1O2P3Q4R5S6T7U8V9W0X1Y2Z3A4B5C6D7E8F9G0H1I2J3K4L5M6N7O8P9Q0R1S2T 3U4V5W6X7Y8Z9A0B1C2D3E4F5G6H7I8J9K0L1M2N3O4P5Q6R7S8T9U0V1W2X3Y4Z 5A6B7C8D9E0F1G2H3I4J5K6L7M8N9O6P7Q8R9S0T1U2V3W4X5Y6Z7A8B9C0D1E2F 7G8H9I0J1K2L3M4N5O6P7Q8R9S0T1U2V3W4X5Y6Z7A8B9C0D1E2F3G4H5I6J7K8L QIDVAQABJQRJQJVJQJkjJ2kj2Jkj2kj2kjJkj2kJJk2jkj2kj2kj2J2kj2kjkj2 -----END RSA PRIVATE KEY-----`; beforeEach(async () => { connectionManager = new SSHConnectionManager(); // Create test directory if (!fsSync.existsSync(testKeyDir)) { await fs.mkdir(testKeyDir, { recursive: true }); } }); afterEach(async () => { connectionManager.cleanup(); // Clean up test directory if (fsSync.existsSync(testKeyDir)) { await fs.rm(testKeyDir, { recursive: true, force: true }); } }); describe("Path Traversal Security Tests", () => { it("should fail - path traversal attack through .. sequences", async () => { // Create a valid key file outside the expected location const sensitiveFile = path.join(os.tmpdir(), "sensitive-key.pem"); await fs.writeFile(sensitiveFile, validTestKey); const config: SSHConnectionConfig = { name: "path-traversal-test", host: "localhost", username: "testuser", // This should be blocked by path traversal protection keyFilePath: `~/../../tmp/sensitive-key.pem` }; try { await connectionManager.createConnection(config); fail("Should have thrown error for path traversal attempt"); } catch (error) { // Currently this attack might succeed - we need protection // After fix, should throw specific security error const errorMessage = (error as Error).message; expect(errorMessage).toContain("Invalid path"); // Should be blocked } finally { // Clean up sensitive file if (fsSync.existsSync(sensitiveFile)) { await fs.unlink(sensitiveFile); } } }); it("should fail - absolute path traversal attack", async () => { // Try to access system files through absolute path const config: SSHConnectionConfig = { name: "absolute-traversal-test", host: "localhost", username: "testuser", keyFilePath: "/etc/passwd" // Should be blocked }; try { await connectionManager.createConnection(config); fail("Should have thrown error for absolute path attack"); } catch (error) { // Should be blocked by validation expect((error as Error).message).toContain("Invalid path"); } }); it("should fail - symlink-based path traversal attack", async () => { // Create a malicious symlink const maliciousLink = path.join(testKeyDir, "malicious-link"); const targetFile = "/etc/hostname"; // System file try { await fs.symlink(targetFile, maliciousLink); const config: SSHConnectionConfig = { name: "symlink-traversal-test", host: "localhost", username: "testuser", keyFilePath: maliciousLink }; await connectionManager.createConnection(config); fail("Should have thrown error for symlink traversal attack"); } catch (error) { // Should detect and block symlink attacks expect((error as Error).message).toContain("Invalid path"); } }); }); describe("Input Validation Tests", () => { it("should fail - null keyFilePath validation", async () => { const config: SSHConnectionConfig = { name: "null-path-test", host: "localhost", username: "testuser", keyFilePath: null as any // Invalid input }; try { await connectionManager.createConnection(config); fail("Should have thrown error for null keyFilePath"); } catch (error) { expect((error as Error).message).toContain("Invalid keyFilePath"); } }); it("should fail - empty string keyFilePath validation", async () => { const config: SSHConnectionConfig = { name: "empty-path-test", host: "localhost", username: "testuser", keyFilePath: "" // Invalid input }; try { await connectionManager.createConnection(config); fail("Should have thrown error for empty keyFilePath"); } catch (error) { expect((error as Error).message).toContain("Invalid keyFilePath"); } }); it("should fail - whitespace-only keyFilePath validation", async () => { const config: SSHConnectionConfig = { name: "whitespace-path-test", host: "localhost", username: "testuser", keyFilePath: " \t\n " // Invalid input }; try { await connectionManager.createConnection(config); fail("Should have thrown error for whitespace-only keyFilePath"); } catch (error) { expect((error as Error).message).toContain("Invalid keyFilePath"); } }); it("should fail - extremely long keyFilePath validation", async () => { const longPath = "a".repeat(5000); // Extremely long path const config: SSHConnectionConfig = { name: "long-path-test", host: "localhost", username: "testuser", keyFilePath: longPath }; try { await connectionManager.createConnection(config); fail("Should have thrown error for excessively long keyFilePath"); } catch (error) { expect((error as Error).message).toContain("Invalid keyFilePath"); } }); }); describe("Error Message Sanitization Tests", () => { it("should fail - system path leakage in error messages", async () => { const sensitiveSystemPath = "/home/secret-user/.ssh/id_rsa"; const config: SSHConnectionConfig = { name: "path-leak-test", host: "localhost", username: "testuser", keyFilePath: sensitiveSystemPath }; try { await connectionManager.createConnection(config); fail("Should have thrown error"); } catch (error) { const errorMessage = (error as Error).message; // Error message should NOT contain the full system path expect(errorMessage).not.toContain("/home/secret-user/.ssh/id_rsa"); // Should contain generic message instead expect(errorMessage).toContain("Key file not accessible"); } }); it("should fail - home directory path leakage", async () => { const homeDir = os.homedir(); const privatePath = path.join(homeDir, ".private", "secret-key"); const config: SSHConnectionConfig = { name: "home-leak-test", host: "localhost", username: "testuser", keyFilePath: privatePath }; try { await connectionManager.createConnection(config); fail("Should have thrown error"); } catch (error) { const errorMessage = (error as Error).message; // Should not leak user's home directory structure expect(errorMessage).not.toContain(homeDir); expect(errorMessage).not.toContain(".private"); } }); }); describe("Async Consistency Tests", () => { it("should fail - readPrivateKeyFromFile should use async file operations", async () => { const testKeyPath = path.join(testKeyDir, "async-test-key"); await fs.writeFile(testKeyPath, validTestKey); const config: SSHConnectionConfig = { name: "async-consistency-test", host: "localhost", username: "testuser", keyFilePath: testKeyPath }; // Monitor the execution to see if it blocks const startTime = Date.now(); try { await connectionManager.createConnection(config); fail("Connection should fail (no actual SSH server)"); } catch (error) { const executionTime = Date.now() - startTime; // With proper async operations, file I/O shouldn't block significantly // This is a heuristic test - proper async should be fast for file operations expect(executionTime).toBeLessThan(100); // Should be near-instantaneous for file I/O // Error should be connection-related, not file-related // Parsing error is also acceptable as it means the file was read successfully expect((error as Error).message).toMatch( /(connection|authentication|timeout|ECONNREFUSED|parse|Malformed)/i ); } }); it("should fail - async declaration without async usage is misleading", () => { // This test verifies the method signature matches implementation // If method is declared async, it should use async operations // Access the private method through type assertion for testing const manager = connectionManager as any; const method = manager.readPrivateKeyFromFile; // Method should be async (returns Promise) expect(method.constructor.name).toBe("AsyncFunction"); // Test actual async behavior by checking if it returns a promise const testPath = path.join(testKeyDir, "test.key"); fsSync.writeFileSync(testPath, validTestKey); const result = method.call(manager, testPath); expect(result).toBeInstanceOf(Promise); }); }); describe("Security Integration Tests", () => { it("should pass - legitimate tilde expansion should work", async () => { const homeDir = os.homedir(); const testKeyInHome = path.join(homeDir, ".test-ssh-key-temp"); try { await fs.writeFile(testKeyInHome, validTestKey); const config: SSHConnectionConfig = { name: "legitimate-tilde-test", host: "localhost", username: "testuser", keyFilePath: "~/.test-ssh-key-temp" }; await connectionManager.createConnection(config); fail("Should fail with connection error, not path error"); } catch (error) { // Should fail with connection/auth error, not path validation error const errorMessage = (error as Error).message; expect(errorMessage).not.toContain("Invalid path"); // Parsing error is also acceptable as it means the file was read successfully expect(errorMessage).toMatch(/(connection|authentication|timeout|parse|Malformed)/i); } finally { if (fsSync.existsSync(testKeyInHome)) { await fs.unlink(testKeyInHome); } } }); it("should pass - relative paths in safe directory should work", async () => { const relativeKeyPath = path.join(testKeyDir, "relative-test-key"); await fs.writeFile(relativeKeyPath, validTestKey); const config: SSHConnectionConfig = { name: "relative-path-test", host: "localhost", username: "testuser", keyFilePath: relativeKeyPath }; try { await connectionManager.createConnection(config); fail("Should fail with connection error"); } catch (error) { // Should process the file successfully and fail on connection expect((error as Error).message).not.toContain("Invalid path"); } }); }); });

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