{"record":{"id":"b4b42501da04f23e","repo":"can1357/oh-my-pi","slug":"plugin-entry-name-dapadapters-path-escapes-th","errorCode":null,"errorMessage":"Plugin \"${entry.name}\" dapAdapters path escapes the plugin directory","messagePattern":"Plugin \"(.+?)\" dapAdapters path escapes the plugin directory","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/extensibility/plugins/marketplace/manager.ts","lineNumber":396,"sourceCode":"\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}\n\t\t\tconst extension = path.extname(sourcePath).toLowerCase();\n\t\t\tconst targetFilename = extension === \".yaml\" || extension === \".yml\" ? `.dap${extension}` : \".dap.json\";\n\t\t\tconst targetPath = path.join(cachePath, targetFilename);\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\tconst targetPath = path.join(cachePath, \".dap.json\");\n\t\tawait Bun.write(targetPath, `${JSON.stringify({ adapters: dapAdapters }, null, 2)}\\n`);\n\t}\n\n\t/**\n\t * Resolve plugin version from multiple sources:\n\t * 1. Catalog entry version (if set)\n\t * 2. Plugin manifest (.claude-plugin/plugin.json, Agent Plugins root plugin.json, or package.json)\n\t * 3. Git SHA from source (truncated to 7 chars)","sourceCodeStart":378,"sourceCodeEnd":414,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/extensibility/plugins/marketplace/manager.ts#L378-L414","documentation":"Analogous to the lspServers check: when a plugin's dapAdapters field is a string, it names a debug-adapter config file (JSON or YAML) resolved against the installed plugin directory. #writeEmbeddedDapConfig enforces pathIsWithin(cachePath, sourcePath) so a plugin cannot read config files from outside its own directory, then copies it to .dap.json/.dap.yaml/.dap.yml inside the plugin cache path.","triggerScenarios":"installPlugin on a plugin whose catalog entry sets dapAdapters to a string path that resolves outside cachePath — e.g. \"../dap/adapters.yaml\" or an absolute path elsewhere on disk.","commonSituations":"Plugin authors referencing a shared debug-adapter config in a sibling directory; absolute paths baked into manifests from another machine; symlinks escaping the plugin directory.","solutions":["Point dapAdapters at a config file inside the plugin directory (\"./dap.json\" or \"./dap.yaml\")","Inline the debug adapter definitions as an object in the dapAdapters field instead of a path","Copy the YAML/JSON config into the plugin's own directory and republish","Ask the maintainer to fix the manifest path"],"exampleFix":"// before (catalog entry)\n\"dapAdapters\": \"../../debug/adapters.yaml\"\n// after\n\"dapAdapters\": \"./dap.yaml\"  // or an inline object of adapter definitions","handlingStrategy":"validation","validationCode":"import * as path from \"node:path\";\nfunction dapPathIsContained(cachePath: string, dapAdapters: string | object | undefined): boolean {\n  if (typeof dapAdapters !== \"string\") return true;\n  const resolved = path.resolve(cachePath, dapAdapters);\n  return resolved === cachePath || resolved.startsWith(cachePath + path.sep);\n}\nif (!dapPathIsContained(cachePath, entry.dapAdapters)) throw new Error(\"dapAdapters escapes plugin dir\");","typeGuard":"function isContainedDapAdapters(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(\"dapAdapters path escapes\")) {\n    logger.error(\"Refusing plugin: insecure dapAdapters path\", { name, marketplace });\n  } else throw err;\n}","preventionTips":["Keep dap adapter config files inside each plugin's directory","Avoid absolute paths in dapAdapters — they almost never transfer between machines","Review plugin manifests for traversal-looking paths before installing untrusted plugins","Use inline object form of dapAdapters when possible to eliminate path resolution entirely"],"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"}