{"record":{"id":"01292132d0567625","repo":"paperclipai/paperclip","slug":"createos-archive-contains-unsafe-entries-or-exceeds-the","errorCode":null,"errorMessage":"CreateOS archive contains unsafe entries or exceeds the extraction limit.","messagePattern":"CreateOS archive contains unsafe entries or exceeds the extraction limit\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/plugins/sandbox-providers/createos/src/file-sync.ts","lineNumber":49,"sourceCode":"export async function validateArchive(file: string): Promise<number> {\n  let invalid = false;\n  let bytes = 0;\n  let files = 0;\n  const inside = (entryPath: string) => !path.posix.isAbsolute(entryPath) &&\n    !entryPath.split(\"/\").includes(\"..\") && !entryPath.includes(\"\\\\\") && !entryPath.includes(\"\\0\");\n  await tar.t({ file, strict: true, onReadEntry(entry) {\n    bytes += entry.size;\n    if (bytes > 10 * 1024 ** 3 || !inside(entry.path)) invalid = true;\n    if (![\"File\", \"OldFile\", \"Directory\", \"SymbolicLink\", \"Link\"].includes(entry.type)) invalid = true;\n    if (entry.type === \"File\" || entry.type === \"OldFile\") files++;\n    if (entry.type === \"SymbolicLink\" || entry.type === \"Link\") {\n      if (!entry.linkpath) { invalid = true; return; }\n      const base = entry.type === \"SymbolicLink\" ? path.posix.dirname(entry.path) : \".\";\n      const target = path.posix.normalize(path.posix.join(base, entry.linkpath));\n      if (path.posix.isAbsolute(entry.linkpath) || entry.linkpath.includes(\"\\\\\") || !inside(target)) invalid = true;\n    }\n  } });\n  if (invalid) throw new Error(\"CreateOS archive contains unsafe entries or exceeds the extraction limit.\");\n  return files;\n}\n\nfunction excluded(name: string, patterns: string[]): boolean {\n  name = name.replace(/^\\.\\//, \"\").replace(/\\/$/, \"\");\n  return patterns.some((pattern) => [pattern, `${pattern}/**`, `**/${pattern}`, `**/${pattern}/**`]\n    .some((glob) => path.matchesGlob(name, glob)));\n}\n\nexport async function syncFiles(\n  client: CreateosClient,\n  params: PluginEnvironmentSyncInParams,\n  direction: \"in\" | \"out\",\n  signal: AbortSignal,\n): Promise<PluginEnvironmentSyncResult> {\n  const id = identifier(params.lease.providerLeaseId);\n  const operations: PluginEnvironmentSyncResult[\"operations\"] = [];\n  const run = async (command: string, cwd = ROOT, timeoutMs?: number) => {","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/packages/plugins/sandbox-providers/createos/src/file-sync.ts#L31-L67","documentation":"validateArchive scans the tar archive built for upload into the sandbox. If any entry is unsafe (absolute link targets, backslashes in linkpath, link targets resolving outside the extraction root, missing linkpath) or the archive exceeds the entry/size extraction limit, the transfer is refused before upload. This prevents symlink attacks and zip-bomb style extractions inside the sandbox.","triggerScenarios":"syncFiles produces an archive containing a symlink whose linkpath is absolute or contains backslashes, a symlink target that normalizes outside the base directory, a hardlink/symlink entry with no linkpath, or an archive exceeding configured entry-count/size limits.","commonSituations":"Syncing directories that contain symlinks (node_modules, build outputs, git worktrees); archives generated on Windows with backslash separators; excluding the limit via very large or deeply nested source trees; malicious or corrupted archives in untrusted workspaces.","solutions":["Remove or re-point symlinks in the source tree (or exclude them with the sync ignore patterns) before syncing.","Use the excluded/pattern options to skip directories known to contain symlinks (node_modules, .git).","Shrink the transfer: sync fewer files or split large uploads so the archive stays under the extraction limit.","Regenerate the archive with POSIX-compliant tooling so linkpaths use forward slashes and relative targets."],"exampleFix":"// before\nawait syncFiles(lease, [{ local: \"./node_modules\", remote: \"/paperclip-workspace/app/node_modules\" }]);\n// after\nawait syncFiles(lease, [{ local: \"./src\", remote: \"/paperclip-workspace/app/src\" }], { exclude: [\"node_modules\"] });","handlingStrategy":"validation","validationCode":"import tar from \"tar-stream\";\nfunction hasUnsafeLinks(fileList) {\n  // pre-screen source tree for symlinks before archiving\n  return fileList.some((f) => {\n    const st = fs.lstatSync(f);\n    return st.isSymbolicLink();\n  });\n}\nif (hasUnsafeLinks(filesToSync)) console.warn(\"symlinks present; they may be rejected by validateArchive\");","typeGuard":null,"tryCatchPattern":"try {\n  await syncFiles(lease, transfers, { exclude: [\"node_modules\", \".git\"] });\n} catch (e) {\n  if (e.message.includes(\"unsafe entries or exceeds the extraction limit\")) {\n    log.error(\"archive rejected: check symlinks and archive size\");\n  }\n  throw e;\n}","preventionTips":["Exclude symlink-heavy directories (node_modules, .git, build outputs) from syncs.","Keep archive size/entry count within limits by syncing incrementally.","Generate archives with POSIX-compliant tooling (forward-slash, relative link targets)."],"tags":["archive","security","symlink","file-transfer"],"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-22T16:17:23.217Z"}