Skip to content

[Security] Path traversal via symlink - FS_BASE_DIRECTORY bypass #16

@xylove21

Description

@xylove21

Vulnerability Summary

Type: Path Traversal via Symlink Escape
Severity: High (CVSS 3.1: 8.1)
File: src/mcp-server/state.ts
Function: resolvePath()
Lines: ~128-145


Description

The resolvePath() function uses path.normalize() with string prefix matching to enforce the FS_BASE_DIRECTORY boundary. However, it does NOT resolve symbolic links before performing this check.

An attacker who can write files within the allowed directory can create symbolic links pointing to files outside the boundary (e.g., /etc/passwd, /root/.ssh/*, /proc/*/environ), then use the MCP tool to read them through the symlink, bypassing the security check entirely.

Proof of Concept

  1. Set FS_BASE_DIRECTORY=/allowed/dir
  2. Create a symlink inside the allowed directory:
    ln -s /etc/passwd /allowed/dir/evil_link
  3. Call the MCP tool with path evil_link
  4. The server reads /etc/passwd contents, bypassing the boundary check

Root Cause

// Current code - vulnerable
if (this.fsBaseDirectory) {
  const normalizedFsBaseDirectory = path.normalize(this.fsBaseDirectory);
  const normalizedSanitizedAbsolutePath = path.normalize(sanitizedAbsolutePath);
  
  // String prefix check - NO symlink resolution!
  if (!normalizedSanitizedAbsolutePath.startsWith(normalizedFsBaseDirectory + path.sep) 
      && normalizedSanitizedAbsolutePath !== normalizedFsBaseDirectory) {
    throw new McpError(...);
  }
}

path.normalize() and string prefix matching do not resolve symbolic links. The fs.readFile() call follows the symlink at read time, bypassing the boundary check.

Impact

  • Read arbitrary files on the server (passwd, SSH keys, credentials, environment variables)
  • Information disclosure enabling further attacks
  • Potential secret/key leakage via /proc or .ssh directories

Recommended Fix

Resolve symlinks before the boundary check:

// Fixed code - safe
const realPath = fs.realpathSync(sanitizedAbsolutePath);
const normalizedRealPath = path.normalize(realPath);

if (!normalizedRealPath.startsWith(normalizedFsBaseDirectory + path.sep) 
    && normalizedRealPath !== normalizedFsBaseDirectory) {
  throw new McpError(...);
}

Timeline

  • Discovery Date: 2026-05-06
  • Report Date: 2026-05-06

This report is submitted in good faith with the goal of improving security. I request coordinated disclosure and will credit the finding as specified by your project security policy.

Best regards,
Security Researcher

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions