Merge remote-tracking branch 'origin/main' into feat/url-elicitation

This commit is contained in:
evalstate
2026-02-19 20:53:58 +00:00
4 changed files with 76 additions and 13 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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[] = [];

View File

@@ -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<string | null> {
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;