{"record":{"id":"c107247919794d1a","repo":"paperclipai/paperclip","slug":"paperclip-runner-attachment-staging-path-denied","errorCode":"paperclip_runner_attachment_staging_path_denied","errorMessage":"paperclip_runner_attachment_staging_path_denied","messagePattern":"paperclip_runner_attachment_staging_path_denied","errorType":"error_code","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"server/src/services/native-runtime/native-runner-file-handoff.ts","lineNumber":682,"sourceCode":"        (constants.O_NOFOLLOW ?? 0),\n      0o600,\n    );\n  }\n  let keepOpen = false;\n  let safeToClear = false;\n  try {\n    const descriptorPath = await openedFilePath(handle.fd);\n    const before = await handle.stat();\n    const pathBefore = await lstat(input.destination);\n    if (\n      !before.isFile() ||\n      before.nlink !== 1 ||\n      !isWithin(input.workspaceRoot, descriptorPath) ||\n      descriptorPath !== (await realpath(input.destination)) ||\n      pathBefore.isSymbolicLink() ||\n      !sameFileIdentity(before, pathBefore)\n    ) {\n      throw new Error(\"paperclip_runner_attachment_staging_path_denied\");\n    }\n    safeToClear = true;\n    await handle.truncate(0);\n    await handle.write(input.body, 0, input.body.length, 0);\n    await handle.sync();\n    const after = await handle.stat();\n    const pathAfter = await lstat(input.destination);\n    await assertNoSymlinkComponents(\n      input.workspaceRoot,\n      path.relative(input.workspaceRoot, input.destination),\n    );\n    if (\n      after.size !== input.body.length ||\n      after.nlink !== 1 ||\n      pathAfter.isSymbolicLink() ||\n      !sameFileIdentity(after, pathAfter) ||\n      descriptorPath !== (await realpath(input.destination))\n    ) {","sourceCodeStart":664,"sourceCodeEnd":700,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/native-runtime/native-runner-file-handoff.ts#L664-L700","documentation":"Pre-write integrity gate when staging runner attachment bytes into a confined workspace slot. Before truncating/writing, the runtime verifies the descriptor path is a regular file (nlink===1), inside workspaceRoot, resolves to the same real path as destination, is not a symlink, and retains the same file identity as when opened. Any violation (TOCTOU swap, symlink injection, path escape) aborts with this coded error to protect the staging slot from path-traversal attacks.","triggerScenarios":"stageNativeRunnerAttachmentBytes pipeline where, between open and write, the destination was replaced by a symlink or different inode; descriptorPath resolved outside workspaceRoot; realpath(destination) diverges from descriptorPath; the file has hard links (nlink!==1).","commonSituations":"Concurrent process swapped the staging path; attacker-manipulated symlink in the staging directory; workspace root moved/renamed mid-stage so realpath differs.","solutions":["Ensure nothing else mutates the staging slot during staging (no concurrent writers, no symlinks in the staging dir)","Re-run staging from scratch — the error is a safety abort; a fresh call recreates a clean confined slot","Verify the workspace root path is stable and fully resolved (no symlinked parents)"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"const st = await fs.lstat(dest);\nif (!st.isFile() || st.nlink !== 1 || path.relative(workspaceRoot, await fs.realpath(dest)).startsWith(\"..\")) {\n  throw new Error(\"staging destination pre-check failed\");\n}","typeGuard":"const safeStagingSlot = (st, realDest, workspaceRoot) => st.isFile() && st.nlink === 1 && !st.isSymbolicLink() && isWithin(workspaceRoot, realDest);","tryCatchPattern":"try { await stageNativeRunnerAttachmentBytes(input); } catch (e) { if (e.message === \"paperclip_runner_attachment_staging_path_denied\") { /* recreate a clean slot and retry once; investigate concurrent writers */ } else throw e; }","preventionTips":["Do not create symlinks or hard links inside staging directories","Use the registry lock for all staging; never bypass it with direct writes","Keep the workspace root path symlink-free and stable for the process lifetime"],"tags":["security","filesystem","symlink"],"backgroundTag":"path-traversal-blocked","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}