Skip to main content
Glama
pathUtils.test.ts8.21 kB
/** * Unit tests for path security utilities */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { resolveSafePath, isPathSafe, normalizePath } from '../src/utils/pathUtils.js'; import path from 'path'; import fs from 'fs/promises'; import os from 'os'; describe('Path Security Utilities', () => { let testWorkspace: string; beforeEach(async () => { // Create a temporary workspace for testing testWorkspace = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-test-')); }); afterEach(async () => { // Clean up temporary workspace try { await fs.rm(testWorkspace, { recursive: true, force: true }); } catch (error) { // Ignore cleanup errors } }); describe('normalizePath', () => { it('should normalize an existing path', async () => { const result = await normalizePath(testWorkspace); expect(result).toBe(path.normalize(testWorkspace)); }); it('should normalize a non-existent path', async () => { const nonExistent = path.join(testWorkspace, 'does-not-exist'); const result = await normalizePath(nonExistent); expect(result).toBe(path.normalize(path.resolve(nonExistent))); }); it('should resolve symbolic links', async () => { // Create a file and a symlink to it const targetFile = path.join(testWorkspace, 'target.txt'); const symlinkFile = path.join(testWorkspace, 'link.txt'); await fs.writeFile(targetFile, 'test'); await fs.symlink(targetFile, symlinkFile); const result = await normalizePath(symlinkFile); expect(result).toBe(path.normalize(targetFile)); }); }); describe('isPathSafe', () => { it('should return true for workspace root itself', () => { const result = isPathSafe(testWorkspace, testWorkspace); expect(result).toBe(true); }); it('should return true for paths within workspace', () => { const safePath = path.join(testWorkspace, 'subdir', 'file.txt'); const result = isPathSafe(testWorkspace, safePath); expect(result).toBe(true); }); it('should return false for paths outside workspace', () => { const unsafePath = path.join(testWorkspace, '..', 'outside'); const resolved = path.resolve(unsafePath); const result = isPathSafe(testWorkspace, resolved); expect(result).toBe(false); }); it('should return false for completely different paths', () => { const unsafePath = '/completely/different/path'; const result = isPathSafe(testWorkspace, unsafePath); expect(result).toBe(false); }); it('should handle workspace root with trailing separator', () => { const rootWithSep = testWorkspace + path.sep; const safePath = path.join(testWorkspace, 'file.txt'); const result = isPathSafe(rootWithSep, safePath); expect(result).toBe(true); }); it('should prevent partial directory name matches', () => { // If workspace is /workspace, /workspace-other should not be considered safe const fakeWorkspace = '/workspace'; const similarPath = '/workspace-other/file.txt'; const result = isPathSafe(fakeWorkspace, similarPath); expect(result).toBe(false); }); }); describe('resolveSafePath with valid paths', () => { it('should resolve a simple relative path', async () => { const result = await resolveSafePath(testWorkspace, 'file.txt'); expect(result).toBe(path.join(testWorkspace, 'file.txt')); }); it('should resolve a nested relative path', async () => { const result = await resolveSafePath(testWorkspace, 'subdir/nested/file.txt'); expect(result).toBe(path.join(testWorkspace, 'subdir', 'nested', 'file.txt')); }); it('should resolve current directory reference', async () => { const result = await resolveSafePath(testWorkspace, './file.txt'); expect(result).toBe(path.join(testWorkspace, 'file.txt')); }); it('should resolve parent directory within workspace', async () => { const result = await resolveSafePath(testWorkspace, 'subdir/../file.txt'); expect(result).toBe(path.join(testWorkspace, 'file.txt')); }); it('should resolve absolute path within workspace', async () => { const absolutePath = path.join(testWorkspace, 'file.txt'); const result = await resolveSafePath(testWorkspace, absolutePath); expect(result).toBe(absolutePath); }); }); describe('resolveSafePath with edge cases', () => { it('should throw error for empty workspace root', async () => { await expect(resolveSafePath('', 'file.txt')).rejects.toThrow( 'Workspace root cannot be empty' ); }); it('should throw error for empty requested path', async () => { await expect(resolveSafePath(testWorkspace, '')).rejects.toThrow( 'Requested path cannot be empty' ); }); it('should handle root path (workspace root itself)', async () => { const result = await resolveSafePath(testWorkspace, '.'); expect(result).toBe(testWorkspace); }); it('should handle deeply nested paths', async () => { const deepPath = 'a/b/c/d/e/f/g/h/i/j/file.txt'; const result = await resolveSafePath(testWorkspace, deepPath); expect(result).toBe(path.join(testWorkspace, deepPath)); }); }); describe('resolveSafePath with traversal attempts', () => { it('should reject parent directory traversal', async () => { await expect(resolveSafePath(testWorkspace, '../outside.txt')).rejects.toThrow( /outside the workspace boundary/ ); }); it('should reject multiple parent directory traversals', async () => { await expect(resolveSafePath(testWorkspace, '../../outside.txt')).rejects.toThrow( /outside the workspace boundary/ ); }); it('should reject traversal after valid path', async () => { await expect(resolveSafePath(testWorkspace, 'subdir/../../outside.txt')).rejects.toThrow( /outside the workspace boundary/ ); }); it('should reject absolute paths outside workspace', async () => { await expect(resolveSafePath(testWorkspace, '/etc/passwd')).rejects.toThrow( /outside the workspace boundary/ ); }); it('should reject absolute paths to different locations', async () => { const differentPath = path.join(os.tmpdir(), 'different-location'); await expect(resolveSafePath(testWorkspace, differentPath)).rejects.toThrow( /outside the workspace boundary/ ); }); }); describe('resolveSafePath with symbolic links', () => { it('should allow symlink within workspace pointing to workspace', async () => { // Create a file and a symlink within workspace const targetFile = path.join(testWorkspace, 'target.txt'); const symlinkFile = path.join(testWorkspace, 'link.txt'); await fs.writeFile(targetFile, 'test'); await fs.symlink(targetFile, symlinkFile); const result = await resolveSafePath(testWorkspace, 'link.txt'); expect(result).toBe(targetFile); }); it('should reject symlink pointing outside workspace', async () => { // Create a symlink that points outside the workspace const outsideTarget = path.join(os.tmpdir(), 'outside-target.txt'); const symlinkFile = path.join(testWorkspace, 'bad-link.txt'); await fs.writeFile(outsideTarget, 'test'); await fs.symlink(outsideTarget, symlinkFile); await expect(resolveSafePath(testWorkspace, 'bad-link.txt')).rejects.toThrow( /symbolic link.*outside the workspace boundary/ ); // Cleanup await fs.unlink(outsideTarget); }); it('should reject symlink to parent directory', async () => { // Create a symlink to parent directory const parentDir = path.dirname(testWorkspace); const symlinkDir = path.join(testWorkspace, 'parent-link'); await fs.symlink(parentDir, symlinkDir); await expect(resolveSafePath(testWorkspace, 'parent-link/file.txt')).rejects.toThrow( /symbolic link.*outside the workspace boundary/ ); }); }); });

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/ShayYeffet/mcp_server'

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