{"record":{"id":"a0cb49fdf02131ac","repo":"paperclipai/paperclip","slug":"refusing-to-activate-payload-outside-paths-insta","errorCode":null,"errorMessage":"Refusing to activate payload outside ${paths.installsRoot}.","messagePattern":"Refusing to activate payload outside (.+?)\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"cli/src/install-store.ts","lineNumber":250,"sourceCode":"\nexport function writeInstallManifestAtomic(\n  manifest: InstallManifest,\n  paths = resolveInstallStorePaths(),\n): void {\n  ensurePrivateDirectory(paths.cliRoot);\n  const temporaryPath = `${paths.manifestPath}.tmp-${process.pid}-${Date.now()}`;\n  try {\n    fs.writeFileSync(temporaryPath, `${JSON.stringify(manifest, null, 2)}\\n`, { mode: 0o600 });\n    fs.renameSync(temporaryPath, paths.manifestPath);\n  } finally {\n    fs.rmSync(temporaryPath, { force: true });\n  }\n}\n\nfunction assertPayloadPath(payloadPath: string, paths: InstallStorePaths): void {\n  const relative = path.relative(paths.installsRoot, path.resolve(payloadPath));\n  if (!relative || relative.startsWith(\"..\") || path.isAbsolute(relative)) {\n    throw new Error(`Refusing to activate payload outside ${paths.installsRoot}.`);\n  }\n  const stat = fs.lstatSync(payloadPath);\n  if (!stat.isDirectory() || stat.isSymbolicLink()) {\n    throw new Error(`Refusing to activate non-directory payload ${payloadPath}.`);\n  }\n  const installsRealPath = fs.realpathSync(paths.installsRoot);\n  const payloadRealPath = fs.realpathSync(payloadPath);\n  if (!payloadRealPath.startsWith(`${installsRealPath}${path.sep}`)) {\n    throw new Error(`Refusing to activate payload that resolves outside ${paths.installsRoot}.`);\n  }\n}\n\nexport function flipCurrentAtomic(\n  payloadPath: string,\n  paths = resolveInstallStorePaths(),\n  hooks: { beforeRename?: () => void } = {},\n): void {\n  assertPayloadPath(payloadPath, paths);","sourceCodeStart":232,"sourceCodeEnd":268,"githubUrl":"https://github.com/paperclipai/paperclip/blob/67001ec6eb96ae601aa27bc91d9b2415d665334a/cli/src/install-store.ts#L232-L268","documentation":"Thrown by assertPayloadPath() (used by flipCurrentAtomic) when the payload path, resolved and made relative to installsRoot, is empty, starts with '..', or is absolute. This prevents activating (symlinking 'current' to) a payload that lives outside the managed installs directory, blocking path-traversal attacks that could point the CLI entrypoint at arbitrary code.","triggerScenarios":"Called flipCurrentAtomic(payloadPath, paths) where payloadPath resolves outside paths.installsRoot — e.g. an absolute path like '/opt/evil', a relative escape like '../../../tmp/x', or empty. assertPayloadPath computes path.relative and detects the escape before any symlink is created.","commonSituations":"1) A caller computed payloadPath from untrusted input without constraining it under installsRoot. 2) Manifest corruption pointing payloadPath outside the store. 3) A bug in install orchestration passing the wrong base path. 4. Tampering attempt to redirect the 'current' symlink at arbitrary code.","solutions":["Ensure payloadPath is always derived from payloadPathFor(paths, source, id) which produces a path under installsRoot, rather than constructed from raw input.","If constructing manually, verify path.resolve(payloadPath) starts with path.resolve(installsRoot) + path.sep before calling flipCurrentAtomic.","If the manifest's payloadPath is wrong, remove the store and reinstall.","Never pass user-supplied absolute paths to flipCurrentAtomic."],"exampleFix":"// before\nflipCurrentAtomic(\"/opt/suspicious/payload\", paths); // throws\n\n// after\nconst id = \"1.2.3\";\nconst payloadPath = payloadPathFor(paths, \"npm\", id); // under installsRoot\nflipCurrentAtomic(payloadPath, paths);","handlingStrategy":"validation","validationCode":"import path from \"node:path\";\nimport { resolveInstallStorePaths } from \"./install-store.js\";\n\nfunction payloadPathWithinInstalls(payloadPath: string, paths = resolveInstallStorePaths()): boolean {\n  const rel = path.relative(paths.installsRoot, path.resolve(payloadPath));\n  return !!rel && !rel.startsWith(\"..\") && !path.isAbsolute(rel);\n}\n\n// Before flipCurrentAtomic:\nif (!payloadPathWithinInstalls(payload, paths)) throw new Error(\"payload outside installs root\");","typeGuard":"import path from \"node:path\";\n\nfunction isWithinBase(base: string, target: string): boolean {\n  const rel = path.relative(base, path.resolve(target));\n  return !!rel && !rel.startsWith(\"..\") && !path.isAbsolute(rel);\n}","tryCatchPattern":"try {\n  flipCurrentAtomic(payloadPath, paths);\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith(\"Refusing to activate payload outside\")) {\n    // payload path was constructed incorrectly; recompute via payloadPathFor\n    throw err;\n  }\n  throw err;\n}","preventionTips":["Always derive payload paths from payloadPathFor(paths, source, id) rather than raw input.","Never accept user-supplied absolute paths for payload activation.","Validate path containment before passing to flipCurrentAtomic."],"tags":["install-store","security","path-traversal","symlink-guard","cli"],"backgroundTag":null,"analyzedSha":"67001ec6eb96ae601aa27bc91d9b2415d665334a","analyzedAt":"2026-08-12T12:05:45.408Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}