{"record":{"id":"36faf051b8904ca8","repo":"paperclipai/paperclip","slug":"daytona-syncout-refusing-tarball-link-whose-target","errorCode":null,"errorMessage":"Daytona syncOut refusing tarball link whose target escapes the extraction dir: ${name} -> ${linkTarget}","messagePattern":"Daytona syncOut refusing tarball link whose target escapes the extraction dir: (.+?) -> (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"packages/plugins/sandbox-providers/daytona/src/file-sync.ts","lineNumber":226,"sourceCode":"    if (typeFlag === \"l\") {\n      const idx = name.indexOf(\" -> \");\n      if (idx === -1) throw new Error(`Daytona syncOut refusing unparseable symlink entry: ${line}`);\n      linkTarget = name.slice(idx + \" -> \".length);\n      name = name.slice(0, idx);\n    } else if (typeFlag === \"h\") {\n      const idx = name.indexOf(\" link to \");\n      if (idx === -1) throw new Error(`Daytona syncOut refusing unparseable hardlink entry: ${line}`);\n      linkTarget = name.slice(idx + \" link to \".length);\n      name = name.slice(0, idx);\n    }\n    const cleanName = name.replace(/\\/+$/, \"\");\n    if (cleanName.length > 0 && posixPathEscapes(cleanName)) {\n      throw new Error(`Daytona syncOut refusing tarball member that escapes the extraction dir: ${name}`);\n    }\n    if (linkTarget !== null) {\n      const resolved = path.posix.join(path.posix.dirname(cleanName), linkTarget);\n      if (path.posix.isAbsolute(linkTarget) || posixPathEscapes(resolved)) {\n        throw new Error(\n          `Daytona syncOut refusing tarball link whose target escapes the extraction dir: ${name} -> ${linkTarget}`,\n        );\n      }\n    }\n  }\n}\n\nasync function extractHostTarball(input: { archivePath: string; localDir: string }): Promise<void> {\n  // The archive is sandbox-authored and untrusted: validate every member (and\n  // link target) is confined before letting host-side tar write a single byte.\n  await assertTarballEntriesConfined(input.archivePath);\n  await fs.mkdir(input.localDir, { recursive: true });\n  await execFileAsync(\"tar\", [\"-xf\", input.archivePath, \"-C\", input.localDir], {\n    env: { ...process.env, COPYFILE_DISABLE: \"1\" },\n    maxBuffer: 32 * 1024 * 1024,\n  });\n}\n","sourceCodeStart":208,"sourceCodeEnd":244,"githubUrl":"https://github.com/paperclipai/paperclip/blob/67001ec6eb96ae601aa27bc91d9b2415d665334a/packages/plugins/sandbox-providers/daytona/src/file-sync.ts#L208-L244","documentation":"Thrown by assertTarballEntriesConfined when a symlink or hardlink member's target, resolved relative to its parent directory, is absolute or escapes the extraction dir. This prevents a follow-up tar member from being written through the link to an arbitrary host path. The guard runs before host-side extraction on the untrusted sandbox-authored archive.","triggerScenarios":"A tarball link entry (typeFlag 'l' or 'h') whose linkTarget is an absolute path, or whose resolved path (dirname(member) joined with linkTarget) normalizes to '..' or '../…'.","commonSituations":"A sandbox plants a symlink pointing to /etc/passwd or ../../etc so a later member write escapes the tree; a legitimate relative link that resolves outside the archive root; misconfigured build artifacts producing links to system paths.","solutions":["Inspect the `name -> linkTarget` in the error to identify the offending link.","Remove or rewrite the sandbox-side symlink/hardlink so its target resolves inside the extraction dir.","If the link is legitimate, restructure it to be a relative in-tree target.","Re-run syncOut once the link target is confined."],"exampleFix":"// before: symlink target escapes (ln -s /etc/passwd ./escape)\n// after: in-tree relative link (ln -s ./nested/data.txt ./shortcut)","handlingStrategy":"validation","validationCode":"function linkTargetIsConfined(memberName: string, linkTarget: string): boolean {\n  if (path.posix.isAbsolute(linkTarget)) return false;\n  const resolved = path.posix.join(path.posix.dirname(memberName.replace(/\\/+$/, '')), linkTarget);\n  const norm = path.posix.normalize(resolved);\n  return !(norm === '..' || norm.startsWith('../'));\n}","typeGuard":null,"tryCatchPattern":"try {\n  await extractHostTarball({ archivePath, localDir });\n} catch (e) {\n  if (e instanceof Error && e.message.includes('link whose target escapes')) {\n    // reject the archive; link target would allow out-of-tree writes\n  }\n  throw e;\n}","preventionTips":["Never disable the tarball link-target confinement check; it prevents symlink-based escapes.","Ensure sandbox symlinks/hardlinks use relative in-tree targets only.","Reject archives with absolute or traversing link targets at the source."],"tags":["daytona","file-sync","tar","security","symlink","path-traversal","syncout"],"backgroundTag":null,"analyzedSha":"67001ec6eb96ae601aa27bc91d9b2415d665334a","analyzedAt":"2026-08-12T12:05:45.408Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}