GitHub Security Advisory: GHSA-p3h2-2j4p-p83g
Package: @samanhappy/mcphub (NPM)
Severity: HIGH (CVSS 0.0)
CWE: CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Vulnerable versions: < 0.12.13
Patched in: 0.12.13

Summary: MCPHub has Path Traversal via Malicious MCPB Manifest Name

Details:
**MCPB File Upload Handler** extracts a ZIP file and reads `manifest.json` from it. The `name` field in the manifest is directly concatenated into a file path (line 107) without any sanitization or path traversal character validation. An attacker can craft a malicious MCPB file where `manifest.name` is set to something like `../../../etc/malicious`, causing the file to be extracted to an arbitrary location on the file system. The `cleanupOldMcpbServer` function (line 110) also uses the unsanitized name, potentially allowing deletion of arbitrary directories.

## 1. Summary
- **Vulnerability Type**: Path Traversal (CWE-22)
- **Sink Location**: src/controllers/mcpbController.ts:107
- **Vulnerability Description**: The `name` field from an uploaded MCPB manifest is used directly, without sanitization or normalization, to construct a file system path for directory creation and move operations, which may lead to path traversal attacks.

## 2. Analysis Logic

### Step 1: Inspect the identified sink (src/controllers/mcpbController.ts:106-116)
I examined the upload handler and located the file system sink where `manifest.name` is used to build the final extraction path and write files to that path.

```ts
// src/controllers/mcpbController.ts:106-116
// Use server name as the final extract directory for automatic version management
const finalExtractDir = path.join(path.dirname(mcpbFilePath), `server-${manifest.name}`);

// Clean up any existing version of this server
cleanupOldMcpbServer(manifest.name);
if (!fs.existsSync(finalExtractDir)) {
  fs.mkdirSync(finalExtractDir, { recursive: true });
}

// Move the temporary directory to the final location
fs.renameSync(tempExtractDir, finalExtractDir);
```

Analysis: `manifest.name` is used to build `finalExtractDir`, which is then operated on by `fs.mkdirSync` and `fs.renameSync`. These are file system write/move operations, so if `name` is user-controlled and unsanitized, this is a path traversal sink. Next, I traced the origin of `manifest.name`.

### Step 2: Trace the source of `manifest.name` in the upload handler (src/controllers/mcpbController.ts:83-104)
I traced back the data flow to see how the manifest is read and validated.

```ts
// src/controllers/mcpbController.ts:83-104
const manifestPath = path.join(tempExtractDir, 'manifest.json');
if (!fs.existsSync(manifestPath)) {
  throw new Error('manifest.json not found in MCPB file');
}

const manifestContent = fs.readFileSync(manifestPath, 'utf-8');
const manifest = JSON.parse(manifestContent);

// Validate required fields in manifest
if (!manifest.manifest_version) {
  throw new Error('Invalid manifest: missing manifest_version');
}
if (!manifest.name) {
  throw new Error('Invalid manifest: missing name');
}
```

Analysis: `manifest` is parsed directly from `manifest.json` inside the uploaded archive. The only check on `manifest.name` is that it is non‑empty; there is no sanitization, normalization, or allow‑list validation. Next, I confirmed the ent