{"record":{"id":"57b51b395d2e700f","repo":"can1357/oh-my-pi","slug":"plugin-entry-name-lspservers-path-escapes-the","errorCode":null,"errorMessage":"Plugin \"${entry.name}\" lspServers path escapes the plugin directory","messagePattern":"Plugin \"(.+?)\" lspServers path escapes the plugin directory","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/extensibility/plugins/marketplace/manager.ts","lineNumber":379,"sourceCode":"\t\t\t}\n\t\t}\n\t\tawait this.#registerRuntimePlugin(scope, packageName, cachePath, version, wasDisabled ? false : undefined);\n\n\t\tthis.#clearCache();\n\n\t\tlogger.debug(\"Plugin installed\", { pluginId, version, cachePath });\n\t\treturn installedEntry;\n\t}\n\n\tasync #writeEmbeddedLspConfig(entry: MarketplacePluginEntry, cachePath: string): Promise<void> {\n\t\tconst lspServers = entry.lspServers;\n\t\tif (!lspServers) return;\n\n\t\tconst targetPath = path.join(cachePath, \".lsp.json\");\n\t\tif (typeof lspServers === \"string\") {\n\t\t\tconst sourcePath = path.resolve(cachePath, lspServers);\n\t\t\tif (!pathIsWithin(cachePath, sourcePath)) {\n\t\t\t\tthrow new Error(`Plugin \"${entry.name}\" lspServers path escapes the plugin directory`);\n\t\t\t}\n\t\t\tconst content = await Bun.file(sourcePath).text();\n\t\t\tawait Bun.write(targetPath, content);\n\t\t\treturn;\n\t\t}\n\n\t\tawait Bun.write(targetPath, `${JSON.stringify({ servers: lspServers }, null, 2)}\\n`);\n\t}\n\n\tasync #writeEmbeddedDapConfig(entry: MarketplacePluginEntry, cachePath: string): Promise<void> {\n\t\tconst dapAdapters = entry.dapAdapters;\n\t\tif (!dapAdapters) return;\n\n\t\tif (typeof dapAdapters === \"string\") {\n\t\t\tconst sourcePath = path.resolve(cachePath, dapAdapters);\n\t\t\tif (!pathIsWithin(cachePath, sourcePath)) {\n\t\t\t\tthrow new Error(`Plugin \"${entry.name}\" dapAdapters path escapes the plugin directory`);\n\t\t\t}","sourceCodeStart":361,"sourceCodeEnd":397,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/extensibility/plugins/marketplace/manager.ts#L361-L397","documentation":"When a plugin's lspServers field is a string, it is treated as a path to an LSP config file resolved relative to the installed plugin directory (cachePath). #writeEmbeddedLspConfig validates with pathIsWithin that the resolved path stays inside the plugin directory, blocking path traversal out of the plugin sandbox. A malicious or buggy plugin manifest referencing ../../secrets/lsp.json is rejected.","triggerScenarios":"Installing (installPlugin) a marketplace plugin whose catalog entry sets lspServers to a string path that, after path.resolve against cachePath, escapes the plugin directory (e.g. \"../shared/lsp.json\", \"/etc/lsp.json\", or a symlinked path outside).","commonSituations":"Plugin authors using ../ to share one LSP config across sibling plugins in a monorepo-style marketplace; absolute paths in manifests that were valid on the author's machine; symlinks inside the plugin dir pointing outward.","solutions":["Fix the plugin's lspServers field to point at a file inside the plugin directory (relative path within it)","Inline the LSP server config directly in the plugin entry instead of referencing an external file","If sharing config across plugins, duplicate the file into each plugin directory or restructure the marketplace so each plugin owns its config","Contact the plugin maintainer to republish with a contained path"],"exampleFix":"// before (catalog entry)\n\"lspServers\": \"../shared/lsp.json\"\n// after\n\"lspServers\": \"./lsp.json\"  // file placed inside this plugin's directory","handlingStrategy":"validation","validationCode":"import * as path from \"node:path\";\nfunction lspPathIsContained(cachePath: string, lspServers: string | object | undefined): boolean {\n  if (typeof lspServers !== \"string\") return true;\n  const resolved = path.resolve(cachePath, lspServers);\n  return resolved === cachePath || resolved.startsWith(cachePath + path.sep);\n}\nif (!lspPathIsContained(cachePath, entry.lspServers)) throw new Error(\"lspServers escapes plugin dir\");","typeGuard":"function isContainedLspServers(v: unknown, cachePath: string): v is string {\n  return typeof v === \"string\" && (path.resolve(cachePath, v) === cachePath || path.resolve(cachePath, v).startsWith(cachePath + path.sep));\n}","tryCatchPattern":"try {\n  await manager.installPlugin(name, marketplace);\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"lspServers path escapes\")) {\n    logger.error(\"Refusing plugin: insecure lspServers path\", { name, marketplace });\n  } else throw err;\n}","preventionTips":["Only install plugins from marketplaces you trust; this error usually flags a malicious or sloppy manifest","As a plugin author, keep all referenced config files inside the plugin directory","Never use ../ or absolute paths in lspServers fields","Audit third-party plugin manifests for path fields before installing"],"tags":["security","path-traversal","plugin-marketplace"],"backgroundTag":"path-escapes-plugin-directory","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}