diff --git a/SECURITY.md b/SECURITY.md index bdc931e1..2d6cdc2b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,16 +1,21 @@ # Security Policy -Thank you for helping us keep our MCP servers secure. -The **reference servers** in this repo are maintained by [Anthropic](https://www.anthropic.com/) as part of the Model Context Protocol project. - -The security of our systems and user data is Anthropic's top priority. We appreciate the work of security researchers acting in good faith in identifying and reporting potential vulnerabilities. +Thank you for helping keep the Model Context Protocol and its ecosystem secure. ## Important Notice -The servers in this repository are **reference implementations** intended to demonstrate MCP features and SDK usage. They serve as educational examples for developers building their own MCP servers, not as production-ready solutions. +The servers in this repository are **reference implementations** intended to demonstrate +MCP features and SDK usage. They serve as educational examples for developers building +their own MCP servers, not as production-ready solutions. -**Bug bounties are not awarded for security vulnerabilities found in these reference servers.** Our bug bounty program applies exclusively to the [MCP SDKs](https://github.com/modelcontextprotocol) maintained by Anthropic. If you discover a vulnerability in an MCP SDK that is maintained by Anthropic, please report it through our vulnerability disclosure program below. +This repository is **not** eligible for security vulnerability reporting. If you discover +a vulnerability in an MCP SDK, please report it in the appropriate SDK repository. -## Vulnerability Disclosure Program +## Reporting Security Issues in MCP SDKs -Our Vulnerability Program guidelines are defined on our [HackerOne program page](https://hackerone.com/anthropic-vdp). We ask that any validated vulnerability in this functionality be reported through the [submission form](https://hackerone.com/anthropic-vdp/reports/new?type=team&report_type=vulnerability). +If you discover a security vulnerability in an MCP SDK, please report it through the +[GitHub Security Advisory process](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability) +in the relevant SDK repository. + +Please **do not** report security vulnerabilities through public GitHub issues, discussions, +or pull requests. diff --git a/src/filesystem/__tests__/path-validation.test.ts b/src/filesystem/__tests__/path-validation.test.ts index 098119ea..81ad247e 100644 --- a/src/filesystem/__tests__/path-validation.test.ts +++ b/src/filesystem/__tests__/path-validation.test.ts @@ -564,6 +564,53 @@ describe('Path Validation', () => { } }); + // Test for macOS /tmp -> /private/tmp symlink issue (GitHub issue #3253) + // When allowed directories include BOTH original and resolved paths, + // paths through either form should be accepted + it('allows paths through both original and resolved symlink directories', async () => { + try { + // Setup: Create the actual target directory with content + const actualTargetDir = path.join(testDir, 'actual-target'); + await fs.mkdir(actualTargetDir, { recursive: true }); + const targetFile = path.join(actualTargetDir, 'file.txt'); + await fs.writeFile(targetFile, 'FILE_CONTENT'); + + // Setup: Create symlink directory that points to target (simulates /tmp -> /private/tmp) + const symlinkDir = path.join(testDir, 'symlink-dir'); + await fs.symlink(actualTargetDir, symlinkDir); + + // Get the resolved path + const resolvedDir = await fs.realpath(symlinkDir); + + // THE FIX: Store BOTH original symlink path AND resolved path in allowed directories + // This is what the server should do during startup to fix issue #3253 + const allowedDirsWithBoth = [symlinkDir, resolvedDir]; + + // Test 1: Path through original symlink should pass validation + // (e.g., user requests /tmp/file.txt when /tmp is in allowed dirs) + const fileViaSymlink = path.join(symlinkDir, 'file.txt'); + expect(isPathWithinAllowedDirectories(fileViaSymlink, allowedDirsWithBoth)).toBe(true); + + // Test 2: Path through resolved directory should also pass validation + // (e.g., user requests /private/tmp/file.txt) + const fileViaResolved = path.join(resolvedDir, 'file.txt'); + expect(isPathWithinAllowedDirectories(fileViaResolved, allowedDirsWithBoth)).toBe(true); + + // Test 3: The resolved path of the symlink file should also pass + const resolvedFile = await fs.realpath(fileViaSymlink); + expect(isPathWithinAllowedDirectories(resolvedFile, allowedDirsWithBoth)).toBe(true); + + // Verify both paths point to the same actual file + expect(resolvedFile).toBe(await fs.realpath(fileViaResolved)); + + } catch (error) { + // Skip if no symlink permissions on the system + if ((error as NodeJS.ErrnoException).code !== 'EPERM') { + throw error; + } + } + }); + it('resolves nested symlink chains completely', async () => { try { // Setup: Create target file in forbidden area diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index 74bf0a92..a515df7c 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -39,22 +39,32 @@ if (args.length === 0) { } // Store allowed directories in normalized and resolved form -let allowedDirectories = await Promise.all( +// We store BOTH the original path AND the resolved path to handle symlinks correctly +// This fixes the macOS /tmp -> /private/tmp symlink issue where users specify /tmp +// but the resolved path is /private/tmp +let allowedDirectories = (await Promise.all( args.map(async (dir) => { const expanded = expandHome(dir); const absolute = path.resolve(expanded); + const normalizedOriginal = normalizePath(absolute); try { // Security: Resolve symlinks in allowed directories during startup // This ensures we know the real paths and can validate against them later const resolved = await fs.realpath(absolute); - return normalizePath(resolved); + const normalizedResolved = normalizePath(resolved); + // Return both original and resolved paths if they differ + // This allows matching against either /tmp or /private/tmp on macOS + if (normalizedOriginal !== normalizedResolved) { + return [normalizedOriginal, normalizedResolved]; + } + return [normalizedResolved]; } catch (error) { // If we can't resolve (doesn't exist), use the normalized absolute path // This allows configuring allowed dirs that will be created later - return normalizePath(absolute); + return [normalizedOriginal]; } }) -); +)).flat(); // Filter to only accessible directories, warn about inaccessible ones const accessibleDirectories: string[] = []; diff --git a/src/filesystem/roots-utils.ts b/src/filesystem/roots-utils.ts index 87329977..5e26bb24 100644 --- a/src/filesystem/roots-utils.ts +++ b/src/filesystem/roots-utils.ts @@ -3,6 +3,7 @@ import path from 'path'; import os from 'os'; import { normalizePath } from './path-utils.js'; import type { Root } from '@modelcontextprotocol/sdk/types.js'; +import { fileURLToPath } from "url"; /** * Converts a root URI to a normalized directory path with basic security validation. @@ -11,7 +12,7 @@ import type { Root } from '@modelcontextprotocol/sdk/types.js'; */ async function parseRootUri(rootUri: string): Promise { try { - const rawPath = rootUri.startsWith('file://') ? rootUri.slice(7) : rootUri; + const rawPath = rootUri.startsWith('file://') ? fileURLToPath(rootUri) : rootUri; const expandedPath = rawPath.startsWith('~/') || rawPath === '~' ? path.join(os.homedir(), rawPath.slice(1)) : rawPath;