Skip to main content
Glama
pathSandboxing.property.test.ts7.82 kB
/** * Property-based tests for path sandboxing * Property 1: Universal path sandboxing * Validates: Requirements 1.4, 2.2, 3.3, 4.4, 5.3, 6.3, 9.1, 9.2 */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { resolveSafePath } from '../src/utils/pathUtils.js'; import * as fc from 'fast-check'; import path from 'path'; import fs from 'fs/promises'; import os from 'os'; describe('Property-Based Tests: Path Sandboxing', () => { let testWorkspace: string; beforeEach(async () => { // Create a temporary workspace for testing testWorkspace = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-pbt-')); }); afterEach(async () => { // Clean up temporary workspace try { await fs.rm(testWorkspace, { recursive: true, force: true }); } catch (error) { // Ignore cleanup errors } }); /** * Property 1: Universal path sandboxing * For any tool and any path parameter, when the resolved path falls outside * the workspace root, the operation should be rejected with a security error. * * Validates: Requirements 1.4, 2.2, 3.3, 4.4, 5.3, 6.3, 9.1, 9.2 */ it('Property 1: should reject all paths that escape workspace boundary', async () => { // Generator for malicious path patterns const maliciousPathGenerator = fc.oneof( // Parent directory traversal patterns fc.constant('..'), fc.constant('../'), fc.constant('../..'), fc.constant('../../'), fc.constant('../../../'), fc.constant('subdir/../../..'), fc.constant('a/b/c/../../../../..'), // Absolute paths outside workspace fc.constant('/etc/passwd'), fc.constant('/tmp/outside'), fc.constant('C:\\Windows\\System32'), fc.constant('/root/.ssh/id_rsa'), // Mixed traversal patterns fc.constant('valid/../../../invalid'), fc.constant('./../../outside'), fc.constant('subdir/../../../etc/passwd'), // Multiple parent traversals fc.tuple( fc.integer({ min: 2, max: 10 }), fc.constantFrom('file.txt', 'secret.key', 'config.json') ).map(([depth, filename]) => { const traversal = Array(depth).fill('..').join(path.sep); return path.join(traversal, filename); }), // Absolute paths to different temp locations fc.string({ minLength: 5, maxLength: 20 }).map(s => path.join(os.tmpdir(), 'different-' + s.replace(/[^a-zA-Z0-9]/g, '')) ) ); await fc.assert( fc.asyncProperty(maliciousPathGenerator, async (maliciousPath) => { // Attempt to resolve the malicious path try { const resolved = await resolveSafePath(testWorkspace, maliciousPath); // If it didn't throw, verify it's actually within workspace // (some paths like 'subdir/../../file.txt' might resolve to workspace) const normalizedWorkspace = path.normalize(testWorkspace); const normalizedResolved = path.normalize(resolved); const workspaceWithSep = normalizedWorkspace.endsWith(path.sep) ? normalizedWorkspace : normalizedWorkspace + path.sep; const isWithinWorkspace = normalizedResolved === normalizedWorkspace || normalizedResolved.startsWith(workspaceWithSep); // If the path resolved successfully, it MUST be within workspace expect(isWithinWorkspace).toBe(true); } catch (error: any) { // Expected: should throw a security violation error expect(error.message).toMatch(/Security violation|outside the workspace/i); } }), { numRuns: 100 } ); }); /** * Additional property test: Valid relative paths should always resolve within workspace */ it('Property 1 (inverse): should accept all valid relative paths within workspace', async () => { // Generator for valid relative paths const validPathGenerator = fc.oneof( // Simple filenames fc.string({ minLength: 1, maxLength: 20 }) .filter(s => !s.includes('..') && !path.isAbsolute(s) && s.trim().length > 0) .map(s => s.replace(/[^a-zA-Z0-9._-]/g, '_') + '.txt'), // Nested paths fc.array( fc.string({ minLength: 1, maxLength: 10 }) .map(s => s.replace(/[^a-zA-Z0-9_-]/g, '_')), { minLength: 1, maxLength: 5 } ).map(parts => parts.join(path.sep) + '.txt'), // Paths with . references that stay within workspace fc.tuple( fc.string({ minLength: 1, maxLength: 10 }).map(s => s.replace(/[^a-zA-Z0-9]/g, '_')), fc.string({ minLength: 1, maxLength: 10 }).map(s => s.replace(/[^a-zA-Z0-9]/g, '_')) ).map(([dir, file]) => path.join(dir, '.', file + '.txt')), // Paths with .. that stay within workspace fc.tuple( fc.string({ minLength: 1, maxLength: 10 }).map(s => s.replace(/[^a-zA-Z0-9]/g, '_')), fc.string({ minLength: 1, maxLength: 10 }).map(s => s.replace(/[^a-zA-Z0-9]/g, '_')), fc.string({ minLength: 1, maxLength: 10 }).map(s => s.replace(/[^a-zA-Z0-9]/g, '_')) ).map(([dir1, dir2, file]) => path.join(dir1, dir2, '..', file + '.txt')) ); await fc.assert( fc.asyncProperty(validPathGenerator, async (validPath) => { // Valid paths should resolve without throwing const resolved = await resolveSafePath(testWorkspace, validPath); // Verify the resolved path is within workspace const normalizedWorkspace = path.normalize(testWorkspace); const normalizedResolved = path.normalize(resolved); const workspaceWithSep = normalizedWorkspace.endsWith(path.sep) ? normalizedWorkspace : normalizedWorkspace + path.sep; const isWithinWorkspace = normalizedResolved === normalizedWorkspace || normalizedResolved.startsWith(workspaceWithSep); expect(isWithinWorkspace).toBe(true); }), { numRuns: 100 } ); }); /** * Additional property test: Absolute paths within workspace should be accepted */ it('Property 1 (absolute paths): should accept absolute paths within workspace', async () => { // Generator for absolute paths within workspace const absolutePathWithinWorkspace = fc.oneof( // Workspace root itself fc.constant(testWorkspace), // Files directly in workspace fc.string({ minLength: 1, maxLength: 20 }) .map(s => s.replace(/[^a-zA-Z0-9._-]/g, '_') + '.txt') .map(filename => path.join(testWorkspace, filename)), // Nested paths within workspace fc.array( fc.string({ minLength: 1, maxLength: 10 }) .map(s => s.replace(/[^a-zA-Z0-9_-]/g, '_')), { minLength: 1, maxLength: 3 } ).map(parts => path.join(testWorkspace, ...parts, 'file.txt')) ); await fc.assert( fc.asyncProperty(absolutePathWithinWorkspace, async (absolutePath) => { // Absolute paths within workspace should resolve successfully const resolved = await resolveSafePath(testWorkspace, absolutePath); // Verify the resolved path is within workspace const normalizedWorkspace = path.normalize(testWorkspace); const normalizedResolved = path.normalize(resolved); const workspaceWithSep = normalizedWorkspace.endsWith(path.sep) ? normalizedWorkspace : normalizedWorkspace + path.sep; const isWithinWorkspace = normalizedResolved === normalizedWorkspace || normalizedResolved.startsWith(workspaceWithSep); expect(isWithinWorkspace).toBe(true); }), { numRuns: 100 } ); }); });

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