Skip to main content
Glama

MCP Gemini Server

by bsmi021
FileSecurityService.vitest.ts16.9 kB
// Using vitest globals - see vitest.config.ts globals: true import * as path from "path"; import * as fs from "fs/promises"; import * as fsSync from "fs"; // Import the code to test import { FileSecurityService } from "../../../src/utils/FileSecurityService.js"; import { ValidationError } from "../../../src/utils/errors.js"; import { logger } from "../../../src/utils/logger.js"; describe("FileSecurityService", () => { // Mock logger const loggerMock = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), }; // Define test constants for all tests const TEST_CONTENT = "Test file content"; const TEST_DIR = path.resolve("./test-security-dir"); const OUTSIDE_DIR = path.resolve("./outside-security-dir"); // Setup before each test beforeEach(() => { // Reset mocks and create test directories vi.clearAllMocks(); // Replace logger with mock vi.spyOn(logger, "info").mockImplementation(loggerMock.info); vi.spyOn(logger, "warn").mockImplementation(loggerMock.warn); vi.spyOn(logger, "error").mockImplementation(loggerMock.error); vi.spyOn(logger, "debug").mockImplementation(loggerMock.debug); // Create test directories fsSync.mkdirSync(TEST_DIR, { recursive: true }); fsSync.mkdirSync(OUTSIDE_DIR, { recursive: true }); }); // Cleanup after each test afterEach(() => { // Restore original logger vi.restoreAllMocks(); // Clean up test directories try { fsSync.rmSync(TEST_DIR, { recursive: true, force: true }); fsSync.rmSync(OUTSIDE_DIR, { recursive: true, force: true }); } catch (err) { // Ignore cleanup errors } }); describe("Constructor and Configuration", () => { it("should initialize with default allowed directories", () => { const service = new FileSecurityService(); const allowedDirs = service.getAllowedDirectories(); expect(allowedDirs.length).toBeGreaterThan(0); expect(allowedDirs).toContain(path.resolve(process.cwd())); }); it("should initialize with custom allowed directories", () => { const customDirs = [TEST_DIR, OUTSIDE_DIR]; const service = new FileSecurityService(customDirs); const allowedDirs = service.getAllowedDirectories(); expect(allowedDirs.length).toBe(2); expect(allowedDirs).toContain(path.resolve(TEST_DIR)); expect(allowedDirs).toContain(path.resolve(OUTSIDE_DIR)); }); it("should initialize with a secure base path", () => { const service = new FileSecurityService([], TEST_DIR); const basePath = service.getSecureBasePath(); expect(basePath).toBe(path.normalize(TEST_DIR)); // Verify allowed directories includes the base path const allowedDirs = service.getAllowedDirectories(); expect(allowedDirs).toContain(path.normalize(TEST_DIR)); }); it("should set allowed directories", () => { const service = new FileSecurityService(); const newDirs = [TEST_DIR, OUTSIDE_DIR]; service.setAllowedDirectories(newDirs); const allowedDirs = service.getAllowedDirectories(); expect(allowedDirs.length).toBe(2); expect(allowedDirs).toContain(path.normalize(TEST_DIR)); expect(allowedDirs).toContain(path.normalize(OUTSIDE_DIR)); }); it("should throw error when setting empty allowed directories", () => { const service = new FileSecurityService(); expect(() => service.setAllowedDirectories([])).toThrow(ValidationError); expect(() => service.setAllowedDirectories([])).toThrow( /At least one allowed directory/ ); }); it("should throw error when setting non-absolute allowed directories", () => { const service = new FileSecurityService(); expect(() => service.setAllowedDirectories(["./relative/path"])).toThrow( ValidationError ); expect(() => service.setAllowedDirectories(["./relative/path"])).toThrow( /Directory path must be absolute/ ); }); it("should set and get secure base path", () => { const service = new FileSecurityService(); service.setSecureBasePath(TEST_DIR); const basePath = service.getSecureBasePath(); expect(basePath).toBe(path.normalize(TEST_DIR)); }); it("should throw error when setting non-absolute secure base path", () => { const service = new FileSecurityService(); expect(() => service.setSecureBasePath("./relative/path")).toThrow( ValidationError ); expect(() => service.setSecureBasePath("./relative/path")).toThrow( /Base path must be absolute/ ); }); it("should configure from environment", () => { // Save original env var const originalEnvVar = process.env.GEMINI_SAFE_FILE_BASE_DIR; // Set env var for test process.env.GEMINI_SAFE_FILE_BASE_DIR = TEST_DIR; const service = FileSecurityService.configureFromEnvironment(); const allowedDirs = service.getAllowedDirectories(); expect(allowedDirs).toContain(path.normalize(TEST_DIR)); // Restore original env var if (originalEnvVar) { process.env.GEMINI_SAFE_FILE_BASE_DIR = originalEnvVar; } else { delete process.env.GEMINI_SAFE_FILE_BASE_DIR; } }); }); describe("Path Validation", () => { let service: FileSecurityService; beforeEach(() => { service = new FileSecurityService([TEST_DIR]); }); it("should validate path within allowed directory", () => { const testFilePath = path.join(TEST_DIR, "test-file.txt"); const validatedPath = service.validateAndResolvePath(testFilePath); expect(validatedPath).toBe(path.normalize(testFilePath)); }); it("should validate paths with relative components", () => { const complexPath = path.join( TEST_DIR, ".", "subdir", "..", "test-file.txt" ); const validatedPath = service.validateAndResolvePath(complexPath); // Should normalize to TEST_DIR/test-file.txt const expectedPath = path.normalize(path.join(TEST_DIR, "test-file.txt")); expect(validatedPath).toBe(expectedPath); }); it("should reject paths outside allowed directories", () => { const outsidePath = path.join(OUTSIDE_DIR, "test-file.txt"); expect(() => service.validateAndResolvePath(outsidePath)).toThrow( ValidationError ); expect(() => service.validateAndResolvePath(outsidePath)).toThrow( /Access denied/ ); }); it("should reject paths with directory traversal", () => { const traversalPath = path.join( TEST_DIR, "..", "outside", "test-file.txt" ); expect(() => service.validateAndResolvePath(traversalPath)).toThrow( ValidationError ); expect(() => service.validateAndResolvePath(traversalPath)).toThrow( /Access denied/ ); }); it("should check file existence with mustExist option", () => { const nonExistentPath = path.join(TEST_DIR, "non-existent.txt"); expect(() => service.validateAndResolvePath(nonExistentPath, { mustExist: true }) ).toThrow(ValidationError); expect(() => service.validateAndResolvePath(nonExistentPath, { mustExist: true }) ).toThrow(/File not found/); }); it("should use custom allowed directories when provided", () => { // Path is outside the service's configured directory but inside custom allowed dir const customAllowedPath = path.join(OUTSIDE_DIR, "custom-allowed.txt"); const validatedPath = service.validateAndResolvePath(customAllowedPath, { allowedDirs: [OUTSIDE_DIR], }); expect(validatedPath).toBe(path.normalize(customAllowedPath)); }); }); describe("isPathWithinAllowedDirs", () => { let service: FileSecurityService; beforeEach(() => { service = new FileSecurityService([TEST_DIR]); }); it("should return true for paths within allowed directories", () => { const insidePath = path.join(TEST_DIR, "test-file.txt"); const result = service.isPathWithinAllowedDirs(insidePath); expect(result).toBe(true); }); it("should return true for exact match with allowed directory", () => { const result = service.isPathWithinAllowedDirs(TEST_DIR); expect(result).toBe(true); }); it("should return false for paths outside allowed directories", () => { const outsidePath = path.join(OUTSIDE_DIR, "test-file.txt"); const result = service.isPathWithinAllowedDirs(outsidePath); expect(result).toBe(false); }); it("should return false for paths with directory traversal", () => { const traversalPath = path.join( TEST_DIR, "..", "outside", "test-file.txt" ); const result = service.isPathWithinAllowedDirs(traversalPath); expect(result).toBe(false); }); it("should use custom allowed directories when provided", () => { const outsidePath = path.join(OUTSIDE_DIR, "test-file.txt"); // Should be false with default allowed dirs expect(service.isPathWithinAllowedDirs(outsidePath)).toBe(false); // Should be true with custom allowed dirs expect(service.isPathWithinAllowedDirs(outsidePath, [OUTSIDE_DIR])).toBe( true ); }); it("should return false when no allowed directories exist", () => { const result = service.isPathWithinAllowedDirs(TEST_DIR, []); expect(result).toBe(false); }); }); describe("fullyResolvePath", () => { let service: FileSecurityService; beforeEach(() => { service = new FileSecurityService([TEST_DIR, OUTSIDE_DIR]); }); it("should resolve a normal file path", async () => { const testPath = path.join(TEST_DIR, "test-file.txt"); const resolvedPath = await service.fullyResolvePath(testPath); expect(resolvedPath).toBe(path.normalize(testPath)); }); it("should handle non-existent paths", async () => { const nonExistentPath = path.join( TEST_DIR, "non-existent", "test-file.txt" ); const resolvedPath = await service.fullyResolvePath(nonExistentPath); expect(resolvedPath).toBe(path.normalize(nonExistentPath)); }); it("should resolve and validate a symlink to a file", async () => { // Create target file const targetPath = path.join(TEST_DIR, "target.txt"); await fs.writeFile(targetPath, TEST_CONTENT, "utf8"); // Create symlink const symlinkPath = path.join(TEST_DIR, "symlink.txt"); await fs.symlink(targetPath, symlinkPath); // Resolve the symlink const resolvedPath = await service.fullyResolvePath(symlinkPath); // Should resolve to the target path expect(resolvedPath).toBe(path.normalize(targetPath)); }); it("should reject symlinks pointing outside allowed directories", async () => { // Create target file in outside (non-allowed) directory const targetPath = path.join(OUTSIDE_DIR, "target.txt"); await fs.writeFile(targetPath, TEST_CONTENT, "utf8"); // Create symlink in test (allowed) directory pointing to outside const symlinkPath = path.join(TEST_DIR, "bad-symlink.txt"); // Setup service with only TEST_DIR allowed (not OUTSIDE_DIR) const restrictedService = new FileSecurityService([TEST_DIR]); await fs.symlink(targetPath, symlinkPath); // Try to resolve the symlink await expect( restrictedService.fullyResolvePath(symlinkPath) ).rejects.toThrow(ValidationError); await expect( restrictedService.fullyResolvePath(symlinkPath) ).rejects.toThrow(/Security error/); await expect( restrictedService.fullyResolvePath(symlinkPath) ).rejects.toThrow(/outside allowed directories/); }); it("should detect and validate symlinked parent directories", async () => { // Create target directory in allowed location const targetDir = path.join(TEST_DIR, "target-dir"); await fs.mkdir(targetDir, { recursive: true }); // Create symlink to directory const symlinkDir = path.join(TEST_DIR, "symlink-dir"); await fs.symlink(targetDir, symlinkDir); // Create a file path inside the symlinked directory const filePath = path.join(symlinkDir, "test-file.txt"); // Resolve the path const resolvedPath = await service.fullyResolvePath(filePath); // Should resolve to actual path in target directory const expectedPath = path.join(targetDir, "test-file.txt"); expect(resolvedPath).toBe(path.normalize(expectedPath)); }); it("should reject symlinked parent directories pointing outside allowed directories", async () => { // Create target directory in outside (not allowed) directory const targetDir = path.join(OUTSIDE_DIR, "target-dir"); await fs.mkdir(targetDir, { recursive: true }); // Create symlink in test directory pointing to outside directory const symlinkDir = path.join(TEST_DIR, "bad-symlink-dir"); await fs.symlink(targetDir, symlinkDir); // Create a file path inside the symlinked directory const filePath = path.join(symlinkDir, "test-file.txt"); // Setup service with only TEST_DIR allowed const restrictedService = new FileSecurityService([TEST_DIR]); // Try to resolve the path await expect( restrictedService.fullyResolvePath(filePath) ).rejects.toThrow(ValidationError); await expect( restrictedService.fullyResolvePath(filePath) ).rejects.toThrow(/Security error/); }); }); describe("secureWriteFile", () => { let service: FileSecurityService; beforeEach(() => { service = new FileSecurityService([TEST_DIR]); }); it("should write file to an allowed directory", async () => { const filePath = path.join(TEST_DIR, "test-file.txt"); await service.secureWriteFile(filePath, TEST_CONTENT); // Verify file was written const content = await fs.readFile(filePath, "utf8"); expect(content).toBe(TEST_CONTENT); }); it("should create directories if they don't exist", async () => { const nestedFilePath = path.join( TEST_DIR, "nested", "deep", "test-file.txt" ); await service.secureWriteFile(nestedFilePath, TEST_CONTENT); // Verify directories were created and file exists const content = await fs.readFile(nestedFilePath, "utf8"); expect(content).toBe(TEST_CONTENT); }); it("should reject writing outside allowed directories", async () => { const outsidePath = path.join(OUTSIDE_DIR, "test-file.txt"); await expect( service.secureWriteFile(outsidePath, TEST_CONTENT) ).rejects.toThrow(ValidationError); await expect( service.secureWriteFile(outsidePath, TEST_CONTENT) ).rejects.toThrow(/Access denied/); // Verify file was not created await expect(fs.access(outsidePath)).rejects.toThrow(); }); it("should reject overwriting existing files by default", async () => { const filePath = path.join(TEST_DIR, "existing-file.txt"); // Create the file first await fs.writeFile(filePath, "Original content", "utf8"); // Try to overwrite without setting overwrite flag await expect( service.secureWriteFile(filePath, TEST_CONTENT) ).rejects.toThrow(ValidationError); await expect( service.secureWriteFile(filePath, TEST_CONTENT) ).rejects.toThrow(/File already exists/); // Verify file wasn't changed const content = await fs.readFile(filePath, "utf8"); expect(content).toBe("Original content"); }); it("should allow overwriting existing files with overwrite flag", async () => { const filePath = path.join(TEST_DIR, "existing-file.txt"); // Create the file first await fs.writeFile(filePath, "Original content", "utf8"); // Overwrite with overwrite flag await service.secureWriteFile(filePath, TEST_CONTENT, { overwrite: true, }); // Verify file was overwritten const content = await fs.readFile(filePath, "utf8"); expect(content).toBe(TEST_CONTENT); }); it("should support custom allowed directories", async () => { // Path is outside the service's configured directories const customAllowedPath = path.join(OUTSIDE_DIR, "custom-allowed.txt"); // Use explicit allowedDirs await service.secureWriteFile(customAllowedPath, TEST_CONTENT, { allowedDirs: [OUTSIDE_DIR], }); // Verify file was written const content = await fs.readFile(customAllowedPath, "utf8"); expect(content).toBe(TEST_CONTENT); }); }); });

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/bsmi021/mcp-gemini-server'

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