{"record":{"id":"8ca880db3a18c209","repo":"stablyai/orca","slug":"verify-packaged-plugin-resources-bundled-plugin-8ca880","errorCode":null,"errorMessage":"[verify-packaged-plugin-resources] bundled plugin path escapes launch root","messagePattern":"\\[verify-packaged-plugin-resources\\] bundled plugin path escapes launch root","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"config/scripts/verify-packaged-plugin-resources.cjs","lineNumber":92,"sourceCode":"  }\n  const index = readJsonFile(join(launchRoot, 'bundled-plugins.json'), 'bundled plugin index')\n  readJsonFile(join(launchRoot, 'orca-marketplace.json'), 'marketplace index')\n  if (index?.version !== 1 || !Array.isArray(index.plugins) || index.plugins.length === 0) {\n    throw new Error('[verify-packaged-plugin-resources] bundled plugin index is empty or invalid')\n  }\n  const resolvedRoot = resolve(launchRoot)\n  for (const entry of index.plugins) {\n    if (\n      typeof entry?.pluginKey !== 'string' ||\n      typeof entry.path !== 'string' ||\n      !/^[0-9a-f]{64}$/.test(entry.contentHash)\n    ) {\n      throw new Error('[verify-packaged-plugin-resources] bundled plugin entry is invalid')\n    }\n    const pluginRoot = resolve(launchRoot, entry.path)\n    const fromRoot = relative(resolvedRoot, pluginRoot)\n    if (!fromRoot || fromRoot === '..' || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot)) {\n      throw new Error('[verify-packaged-plugin-resources] bundled plugin path escapes launch root')\n    }\n    const manifest = readJsonFile(join(pluginRoot, 'orca-plugin.json'), 'plugin manifest')\n    if (`${manifest.publisher}.${manifest.id}` !== entry.pluginKey) {\n      throw new Error(\n        `[verify-packaged-plugin-resources] manifest identity does not match ${entry.pluginKey}`\n      )\n    }\n    if (hashPackagedPluginTree(pluginRoot) !== entry.contentHash) {\n      throw new Error(\n        `[verify-packaged-plugin-resources] packaged bytes do not match ${entry.pluginKey}`\n      )\n    }\n  }\n  console.log(\n    `[verify-packaged-plugin-resources] OK — verified ${index.plugins.length} bundled plugin(s)`\n  )\n}\n","sourceCodeStart":74,"sourceCodeEnd":110,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/config/scripts/verify-packaged-plugin-resources.cjs#L74-L110","documentation":"Thrown by the packaged plugin resource verifier when a plugin entry's path field, resolved against the launch root, escapes the launch directory. The check uses path.relative and path.isAbsolute to detect traversal: if the relative path is empty, equals '..', starts with '../', or is absolute, the plugin path is considered an escape attempt. This is a security guard against path traversal in the plugin index.","triggerScenarios":"An entry in bundled-plugins.json has a path like '../../../etc/passwd', an absolute path like '/usr/local/lib', or an empty path that resolves to the root itself. The resolve(launchRoot, entry.path) call produces a path outside resolvedRoot, and the relative(resolvedRoot, pluginRoot) check catches it.","commonSituations":"A path traversal attack via a maliciously crafted plugin index; a misconfigured indexing script that writes absolute paths instead of relative ones; a plugin path that was manually edited to point outside the launch directory; a symlink dereference during indexing that resolved to an external path.","solutions":["Inspect the path field of the offending entry in bundled-plugins.json — it must be a relative path within the launch directory (e.g., 'my-plugin/', './my-plugin').","Fix the indexing script to only emit relative paths that stay within the launch root.","If the path looks correct but still triggers, check for symlink resolution issues: resolve() may follow symlinks to locations outside the root.","Add a sanitization step in the indexing pipeline that validates paths with the same relative/absolute check before writing them to the index."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Before writing to the index, validate plugin paths stay within launch root.\nconst { resolve, relative, isAbsolute, sep } = require('node:path')\n\nfunction isPathWithinRoot(rootDir, testPath) {\n  const resolved = resolve(rootDir, testPath)\n  const rel = relative(resolve(rootDir), resolved)\n  return (\n    !!rel &&\n    rel !== '..' &&\n    !rel.startsWith(`..${sep}`) &&\n    !isAbsolute(rel)\n  )\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["In the indexing script, validate every plugin path with the same isPathWithinRoot check before writing it to bundled-plugins.json.","Never construct plugin paths from user input without sanitization; use only relative paths derived from the directory scan.","Treat path traversal in a plugin index as a security incident — investigate whether the index was tampered with."],"tags":["plugin-packaging","security","path-traversal","ci-gate","resource-verification"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}