paperclipai/paperclip · critical

Daytona syncOut refusing tarball link whose target escapes t

Error message

Daytona syncOut refusing tarball link whose target escapes the extraction dir: ${name} -> ${linkTarget}

What it means

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.

Source

Thrown at packages/plugins/sandbox-providers/daytona/src/file-sync.ts:226

    if (typeFlag === "l") {
      const idx = name.indexOf(" -> ");
      if (idx === -1) throw new Error(`Daytona syncOut refusing unparseable symlink entry: ${line}`);
      linkTarget = name.slice(idx + " -> ".length);
      name = name.slice(0, idx);
    } else if (typeFlag === "h") {
      const idx = name.indexOf(" link to ");
      if (idx === -1) throw new Error(`Daytona syncOut refusing unparseable hardlink entry: ${line}`);
      linkTarget = name.slice(idx + " link to ".length);
      name = name.slice(0, idx);
    }
    const cleanName = name.replace(/\/+$/, "");
    if (cleanName.length > 0 && posixPathEscapes(cleanName)) {
      throw new Error(`Daytona syncOut refusing tarball member that escapes the extraction dir: ${name}`);
    }
    if (linkTarget !== null) {
      const resolved = path.posix.join(path.posix.dirname(cleanName), linkTarget);
      if (path.posix.isAbsolute(linkTarget) || posixPathEscapes(resolved)) {
        throw new Error(
          `Daytona syncOut refusing tarball link whose target escapes the extraction dir: ${name} -> ${linkTarget}`,
        );
      }
    }
  }
}

async function extractHostTarball(input: { archivePath: string; localDir: string }): Promise<void> {
  // The archive is sandbox-authored and untrusted: validate every member (and
  // link target) is confined before letting host-side tar write a single byte.
  await assertTarballEntriesConfined(input.archivePath);
  await fs.mkdir(input.localDir, { recursive: true });
  await execFileAsync("tar", ["-xf", input.archivePath, "-C", input.localDir], {
    env: { ...process.env, COPYFILE_DISABLE: "1" },
    maxBuffer: 32 * 1024 * 1024,
  });
}

View on GitHub (pinned to 67001ec6eb)

Solutions

  1. Inspect the `name -> linkTarget` in the error to identify the offending link.
  2. Remove or rewrite the sandbox-side symlink/hardlink so its target resolves inside the extraction dir.
  3. If the link is legitimate, restructure it to be a relative in-tree target.
  4. Re-run syncOut once the link target is confined.

Example fix

// before: symlink target escapes (ln -s /etc/passwd ./escape)
// after: in-tree relative link (ln -s ./nested/data.txt ./shortcut)
Defensive patterns

Strategy: validation

Validate before calling

function linkTargetIsConfined(memberName: string, linkTarget: string): boolean {
  if (path.posix.isAbsolute(linkTarget)) return false;
  const resolved = path.posix.join(path.posix.dirname(memberName.replace(/\/+$/, '')), linkTarget);
  const norm = path.posix.normalize(resolved);
  return !(norm === '..' || norm.startsWith('../'));
}

Try / catch

try {
  await extractHostTarball({ archivePath, localDir });
} catch (e) {
  if (e instanceof Error && e.message.includes('link whose target escapes')) {
    // reject the archive; link target would allow out-of-tree writes
  }
  throw e;
}

Prevention

When it happens

Trigger: 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 '../…'.

Common situations: 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.

Related errors


AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12). Data as JSON: /api/errors/36faf051b8904ca8. Report an issue: GitHub.