{"record":{"id":"1941cb439de846b9","repo":"CherryHQ/cherry-studio","slug":"path-traversal-detected-target-path-must-be-direc","errorCode":null,"errorMessage":"Path traversal detected: target path must be direct child of base directory","messagePattern":"Path traversal detected: target path must be direct child of base directory","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ai/mcp/McpPackageService.ts","lineNumber":26,"sourceCode":"\nconst logger = loggerService.withContext('McpPackageService')\n\n/**\n * Ensure a target path is within the base directory to prevent path traversal attacks.\n * This is the correct approach: validate the final resolved path rather than sanitizing input.\n *\n * @param basePath - The base directory that the target must be within\n * @param targetPath - The target path to validate\n * @returns The resolved target path if valid\n * @throws Error if the target path escapes the base directory\n */\nexport function ensurePathWithin(basePath: string, targetPath: string): string {\n  const resolvedBase = path.resolve(basePath)\n  const resolvedTarget = path.resolve(path.normalize(targetPath))\n\n  // Must be direct child of base directory, no subdirectories allowed\n  if (path.dirname(resolvedTarget) !== resolvedBase) {\n    throw new Error('Path traversal detected: target path must be direct child of base directory')\n  }\n\n  return resolvedTarget\n}\n\n/**\n * Guard against zip-slip: `node-stream-zip` writes each entry at `path.join(baseDir, entry.name)`\n * with no containment check, so a name like `../../../foo` would escape `baseDir`. Reject any entry\n * whose resolved destination is outside `baseDir` before extraction. Unlike {@link ensurePathWithin},\n * nested subdirectories are allowed (a DXT archive legitimately contains them).\n *\n * @throws Error if any entry name escapes `baseDir`\n */\nexport function assertZipEntriesWithin(entryNames: string[], baseDir: string): void {\n  const root = path.resolve(baseDir)\n  for (const name of entryNames) {\n    const dest = path.resolve(baseDir, name)\n    if (dest !== root && !dest.startsWith(root + path.sep)) {","sourceCodeStart":8,"sourceCodeEnd":44,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/mcp/McpPackageService.ts#L8-L44","documentation":"Thrown by ensurePathWithin() when the resolved target path's parent directory is not exactly the base directory. Unlike assertZipEntriesWithin (which allows nested subdirectories), this function requires the target to be a DIRECT child — no subdirectories are permitted. It is used exclusively for staging, backup, and final MCP package installation directories that must sit directly under the MCP root directory.","triggerScenarios":"Called from McpPackageService during package installation: staging directories (line 429-430), final extract directory (line 570), and server directory (line 645). Fails when the server directory name contains path separators or '..' sequences that would place the target outside or deeper than the immediate child level of mcpDir.","commonSituations":"A malicious or malformed MCP package manifest specifies a server directory name containing path separators (e.g., 'server/../../escape'); the serverDirName was derived from untrusted user input without sanitization; a platform-specific path separator in the directory name caused cross-OS validation failure.","solutions":["Sanitize serverDirName to remove path separators and '..' segments before calling ensurePathWithin.","Generate the directory name from a UUID or slugified package name rather than trusting manifest input.","Inspect the manifest's name/version fields that feed into serverDirName to confirm they don't contain path characters."],"exampleFix":"// before\nconst serverDir = ensurePathWithin(this.mcpDir, path.join(this.mcpDir, serverDirName))\n\n// after — sanitize the directory name first\nconst safeName = serverDirName.replace(/[/\\\\]/g, '_')\nconst serverDir = ensurePathWithin(this.mcpDir, path.join(this.mcpDir, safeName))","handlingStrategy":"validation","validationCode":"import path from 'node:path'\n\nfunction sanitizeDirName(name: string): string {\n  // Remove path separators and traversal sequences\n  return name.replace(/[/\\\\]/g, '_').replace(/\\.+/g, '.')\n}\n\nconst safeName = sanitizeDirName(serverDirName)\nconst target = ensurePathWithin(mcpDir, path.join(mcpDir, safeName))","typeGuard":"function isDirectChildPath(basePath: string, targetPath: string): boolean {\n  const resolvedBase = path.resolve(basePath)\n  const resolvedTarget = path.resolve(path.normalize(targetPath))\n  return path.dirname(resolvedTarget) === resolvedBase\n}","tryCatchPattern":null,"preventionTips":["Sanitize directory names derived from manifest input — strip path separators and '..' segments.","Generate directory names from UUIDs or slugified package names instead of trusting manifest-provided names.","Test ensurePathWithin with adversarial inputs (../, absolute paths, mixed separators) before relying on it."],"tags":["security","path-traversal","mcp","package-install"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}