{"record":{"id":"09d4c3fdf8ea224c","repo":"paperclipai/paperclip","slug":"refusing-to-activate-payload-that-resolves-outside","errorCode":null,"errorMessage":"Refusing to activate payload that resolves outside ${paths.installsRoot}.","messagePattern":"Refusing to activate payload that resolves outside (.+?)\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"cli/src/install-store.ts","lineNumber":259,"sourceCode":"    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);\n  ensurePrivateDirectory(paths.cliRoot);\n  try {\n    const currentStat = fs.lstatSync(paths.currentPath);\n    if (!currentStat.isSymbolicLink()) {\n      throw new Error(`Refusing to replace non-symlink ${paths.currentPath}.`);\n    }\n  } catch (error) {\n    if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") throw error;\n  }","sourceCodeStart":241,"sourceCodeEnd":277,"githubUrl":"https://github.com/paperclipai/paperclip/blob/67001ec6eb96ae601aa27bc91d9b2415d665334a/cli/src/install-store.ts#L241-L277","documentation":"Thrown by assertPayloadPath inside flipCurrentAtomic when the payload directory's real filesystem path (resolved via fs.realpathSync) does not start with the real path of installsRoot. This is the third layer of defense in a three-stage path-traversal check: lexical containment, directory-type verification, and symlink-resolved containment. It catches cases where a symlink inside the installs root points outside it, defeating the earlier lexical check.","triggerScenarios":"Calling flipCurrentAtomic(payloadPath, paths) where payloadPath is a directory that contains, or is reached through, a symlink chain that resolves outside paths.installsRoot. For example, installsRoot/npm/canary is a symlink to /tmp/evil, or the installsRoot itself is a bind-mount/symlink whose real path differs from its lexical path.","commonSituations":"A previous install was created with a symlinked payload, or the installs directory tree was manually rearranged or symlinked to save disk space. A user or tool moved installsRoot and left a symlink in its place. Cross-filesystem bind mounts where realpath differs from the expected path.","solutions":["Inspect the payload path with 'readlink -f <payloadPath>' and compare it against 'readlink -f <paths.installsRoot>' to find which symlink escapes the root.","Remove or fix the offending symlink so the payload directory genuinely lives under installsRoot.","If the installs root itself is a symlink or bind mount, make paths.installsRoot point at the real path or remove the indirection.","Re-run the install from scratch: remove the install store and let the installer recreate the payload directory natively."],"exampleFix":"// before: payload is a symlink escaping installsRoot\n// installsRoot/npm/canary -> /tmp/some-other-dir\n\n// after: real directory under installsRoot\nfs.rmSync(payloadPath); // remove the symlink\nfs.mkdirSync(payloadPath, { recursive: true }); // create real directory\n// re-extract/install the payload into the real directory","handlingStrategy":"validation","validationCode":"import fs from 'node:fs';\nimport path from 'node:path';\n\nfunction validatePayloadPathSafe(payloadPath: string, installsRoot: string): boolean {\n  try {\n    const installsReal = fs.realpathSync(installsRoot);\n    const payloadReal = fs.realpathSync(payloadPath);\n    return payloadReal.startsWith(`${installsReal}${path.sep}`);\n  } catch {\n    return false;\n  }\n}\n\n// Call before flipCurrentAtomic:\nif (!validatePayloadPathSafe(payloadPath, paths.installsRoot)) {\n  throw new Error('Payload realpath escapes installs root; fix symlinks before activating.');\n}","typeGuard":null,"tryCatchPattern":"try {\n  flipCurrentAtomic(payloadPath, paths);\n} catch (error) {\n  if (error instanceof Error && error.message.includes('resolves outside')) {\n    // Symlink resolution failure: inspect realpaths, fix the symlink chain\n    console.error('Payload symlink escapes installs root:', fs.realpathSync(payloadPath));\n  }\n  throw error;\n}","preventionTips":["Never create symlinks inside the installs root that point outside it.","Always use payloadPathFor() to compute payload paths—it constrains identifiers to [A-Za-z0-9._-] and joins under installsRoot.","After installing a payload, verify with 'readlink -f' that its real path is inside the installs root.","Avoid bind-mounting or symlink-mounting the installs directory tree."],"tags":["security","path-traversal","symlink","install-store","cli"],"backgroundTag":null,"analyzedSha":"67001ec6eb96ae601aa27bc91d9b2415d665334a","analyzedAt":"2026-08-12T12:05:45.408Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}