{"record":{"id":"ed526d16f2405b03","repo":"can1357/oh-my-pi","slug":"marketplace-plugin-package-path-escapes-node-modul","errorCode":null,"errorMessage":"Marketplace plugin package path escapes node_modules: ${JSON.stringify(packageName)}","messagePattern":"Marketplace plugin package path escapes node_modules: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/extensibility/plugins/marketplace/manager.ts","lineNumber":811,"sourceCode":"\t}\n\n\tasync #resolvePluginPackageName(installPath: string, fallbackName: string): Promise<string> {\n\t\ttry {\n\t\t\tconst pkg: { name?: unknown } = await Bun.file(path.join(installPath, \"package.json\")).json();\n\t\t\tconst name = typeof pkg.name === \"string\" && pkg.name.length > 0 ? pkg.name : fallbackName;\n\t\t\treturn assertRuntimePackageName(name);\n\t\t} catch (err) {\n\t\t\tif (isEnoent(err)) return assertRuntimePackageName(fallbackName);\n\t\t\tthrow err;\n\t\t}\n\t}\n\n\t#runtimePackagePath(scope: \"user\" | \"project\", packageName: string): string {\n\t\tconst nodeModules = path.resolve(this.#nodeModulesPath(scope));\n\t\tconst linkPath = path.resolve(nodeModules, assertRuntimePackageName(packageName));\n\t\tconst relative = path.relative(nodeModules, linkPath);\n\t\tif (relative === \"\" || relative.startsWith(\"..\") || path.isAbsolute(relative)) {\n\t\t\tthrow new Error(`Marketplace plugin package path escapes node_modules: ${JSON.stringify(packageName)}`);\n\t\t}\n\t\treturn linkPath;\n\t}\n\n\tasync #resolveInstalledPackageNames(\n\t\tentries: readonly InstalledPluginEntry[],\n\t\tfallbackName: string,\n\t): Promise<Set<string>> {\n\t\tconst packageNames = new Set<string>();\n\t\tfor (const entry of entries) {\n\t\t\tpackageNames.add(await this.#resolvePluginPackageName(entry.installPath, fallbackName));\n\t\t}\n\t\treturn packageNames;\n\t}\n\n\tasync #registerRuntimePlugin(\n\t\tscope: \"user\" | \"project\",\n\t\tpackageName: string,","sourceCodeStart":793,"sourceCodeEnd":829,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/extensibility/plugins/marketplace/manager.ts#L793-L829","documentation":"MarketplaceManager.#runtimePackagePath() computes the symlink path for a plugin's package inside the scope's node_modules directory. It first validates the package name with assertRuntimePackageName(), then double-checks via path.relative() that the resolved link path stays inside node_modules. If the name would escape (e.g. contains \"..\", absolute paths, or is empty), it throws this path-traversal defense error.","triggerScenarios":"A marketplace catalog or installed entry contains a malicious or corrupt packageName such as \"../../evil\", an absolute path like \"/etc\", or an empty string; a manually edited installed-plugins.json with a tampered package field.","commonSituations":"Hand-edited or corrupted registry files; a malicious/misconfigured marketplace publishing a plugin entry with traversal in its package name; symlink-resolution oddities after moving project directories.","solutions":["Inspect the installed-plugins registry / marketplace catalog entry and fix the packageName field to a bare npm-style name","Remove and reinstall the plugin so the registry is rewritten with a valid name","Never hand-edit registry JSON — use the marketplace CLI commands to mutate state","If a third-party marketplace produced this, report/remove that marketplace entry"],"exampleFix":"// before (registry entry)\n{\"packageName\": \"../../evil\"}\n// after\n{\"packageName\": \"my-plugin-pkg\"}","handlingStrategy":"validation","validationCode":"import * as path from 'node:path';\nfunction isSafePackageName(name) {\n  return typeof name === 'string' && /^[a-z0-9@][a-z0-9._\\-/]*$/i.test(name) &&\n    !name.includes('..') && !path.isAbsolute(name);\n}","typeGuard":"const isSafePkgName = (v) => typeof v === 'string' && v.length > 0 && !v.includes('..') && !path.isAbsolute(v);","tryCatchPattern":"try {\n  const linkPath = manager.resolveRuntimePackagePath(scope, pkg);\n} catch (err) {\n  if (err.message.includes('escapes node_modules')) {\n    console.error(`Refusing unsafe package name ${pkg} — registry entry may be corrupted`);\n  } else throw err;\n}","preventionTips":["Never hand-edit installed-plugins.json — mutate via manager APIs","Validate package names when importing registries from untrusted sources","Treat this error as a sign of tampering or corruption, not a code bug","Reinstall the plugin to regenerate a clean registry entry"],"tags":["security","path-traversal","validation"],"backgroundTag":"path-traversal-detected","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}