Skip to main content
Glama
pathUtils.ts6.09 kB
/** * Path security utilities for MCP Workspace Server * Provides path validation and sandboxing to prevent directory traversal attacks */ import path from 'path'; import fs from 'fs/promises'; import { createSecurityError, createInvalidInputError } from './errors.js'; /** * Normalizes a path and resolves symbolic links * @param targetPath - The path to normalize * @returns Normalized absolute path with symlinks resolved */ export async function normalizePath(targetPath: string): Promise<string> { try { // Resolve to absolute path and follow symlinks const realPath = await fs.realpath(targetPath); return path.normalize(realPath); } catch (error) { // If path doesn't exist, normalize without following symlinks // This allows validation of paths that will be created return path.normalize(path.resolve(targetPath)); } } /** * Checks if a resolved path is within the workspace boundary * @param workspaceRoot - The workspace root directory (must be absolute) * @param resolvedPath - The resolved path to check (must be absolute) * @returns true if path is safe, false otherwise */ export function isPathSafe(workspaceRoot: string, resolvedPath: string): boolean { // Normalize both paths to ensure consistent comparison const normalizedRoot = path.normalize(workspaceRoot); const normalizedPath = path.normalize(resolvedPath); // Check if the resolved path starts with the workspace root // Add path separator to prevent partial directory name matches // e.g., /workspace should not match /workspace-other const rootWithSep = normalizedRoot.endsWith(path.sep) ? normalizedRoot : normalizedRoot + path.sep; return normalizedPath === normalizedRoot || normalizedPath.startsWith(rootWithSep); } /** * Resolves a requested path relative to workspace root and validates * it stays within the workspace boundary * @param workspaceRoot - The workspace root directory (absolute path) * @param requestedPath - The requested path (relative or absolute) * @returns Resolved absolute path within workspace * @throws Error if path escapes workspace or contains security violations */ export async function resolveSafePath( workspaceRoot: string, requestedPath: string ): Promise<string> { // Validate inputs if (!workspaceRoot) { throw createInvalidInputError('Workspace root cannot be empty'); } if (!requestedPath) { throw createInvalidInputError('Requested path cannot be empty'); } // Ensure workspace root is absolute const absoluteRoot = path.resolve(workspaceRoot); // Detect absolute paths that might be outside workspace if (path.isAbsolute(requestedPath)) { // If it's an absolute path, we need to check if it's within workspace const normalizedRequested = path.normalize(requestedPath); // Normalize the workspace root for comparison const normalizedRoot = await normalizePath(absoluteRoot); // Check if the absolute path is within workspace if (!isPathSafe(normalizedRoot, normalizedRequested)) { throw createSecurityError( `Path '${requestedPath}' is outside the workspace boundary`, { requestedPath, workspaceRoot: normalizedRoot } ); } // Try to resolve symlinks if path exists const resolvedPath = await normalizePath(normalizedRequested); // Final safety check after symlink resolution if (!isPathSafe(normalizedRoot, resolvedPath)) { throw createSecurityError( `Path '${requestedPath}' resolves to a location outside the workspace boundary`, { requestedPath, resolvedPath, workspaceRoot: normalizedRoot } ); } return resolvedPath; } // Join relative path with workspace root const joinedPath = path.join(absoluteRoot, requestedPath); // Normalize to resolve . and .. segments const normalizedPath = path.normalize(joinedPath); // Normalize workspace root const normalizedRoot = await normalizePath(absoluteRoot); // Check if normalized path is within workspace if (!isPathSafe(normalizedRoot, normalizedPath)) { throw createSecurityError( `Path '${requestedPath}' attempts to traverse outside the workspace boundary`, { requestedPath, normalizedPath, workspaceRoot: normalizedRoot } ); } // Check each component of the path for symlinks that might escape // We need to check parent directories even if the final path doesn't exist let currentCheckPath = normalizedRoot; const relativeParts = path.relative(normalizedRoot, normalizedPath).split(path.sep); for (const part of relativeParts) { if (!part || part === '.') continue; currentCheckPath = path.join(currentCheckPath, part); // Check if this component exists try { await fs.access(currentCheckPath); // Resolve any symlinks at this level const realPath = await fs.realpath(currentCheckPath); // Check if the real path is within workspace if (!isPathSafe(normalizedRoot, realPath)) { throw createSecurityError( `Path '${requestedPath}' contains a symbolic link that points outside the workspace boundary`, { requestedPath, realPath, workspaceRoot: normalizedRoot } ); } // Continue checking from the real path currentCheckPath = realPath; } catch (error: any) { // If ENOENT, path doesn't exist yet - that's fine if (error.code !== 'ENOENT') { throw error; } // For non-existent paths, we've already validated the normalized path // so we can safely continue } } // Try to resolve symlinks if the full path exists const resolvedPath = await normalizePath(normalizedPath); // Final safety check after symlink resolution if (!isPathSafe(normalizedRoot, resolvedPath)) { throw createSecurityError( `Path '${requestedPath}' contains a symbolic link that points outside the workspace boundary`, { requestedPath, resolvedPath, workspaceRoot: normalizedRoot } ); } return resolvedPath; }

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