{"record":{"id":"07684ed42ca65c94","repo":"paperclipai/paperclip","slug":"createos-transfer-path-escapes-the-workspace","errorCode":null,"errorMessage":"CreateOS transfer path escapes the workspace.","messagePattern":"CreateOS transfer path escapes the workspace\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/plugins/sandbox-providers/createos/src/file-sync.ts","lineNumber":19,"sourceCode":"import path from \"node:path\";\nimport os from \"node:os\";\nimport { randomUUID } from \"node:crypto\";\nimport { createReadStream, createWriteStream, promises as fs } from \"node:fs\";\nimport { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport * as tar from \"tar\";\nimport type { PluginEnvironmentSyncInParams, PluginEnvironmentSyncResult } from \"@paperclipai/plugin-sdk\";\nimport { CreateosClient, identifier } from \"./client.js\";\nimport { execute, shellQuote } from \"./execute.js\";\n\nconst ROOT = \"/paperclip-workspace\";\n\nexport function assertRemotePath(value: string): void {\n  if (!path.posix.isAbsolute(value) || value.includes(\"\\0\") || value.split(\"/\").includes(\"..\")) {\n    throw new Error(\"CreateOS transfer requires a confined absolute sandbox path.\");\n  }\n  const normalized = path.posix.normalize(value);\n  if (normalized !== ROOT && !normalized.startsWith(`${ROOT}/`)) throw new Error(\"CreateOS transfer path escapes the workspace.\");\n}\n\nfunction remoteGuard(candidate: string): string {\n  assertRemotePath(candidate);\n  // Re-check symlinks inside the sandbox immediately before use. A missing\n  // canonicalizer fails the command rather than weakening containment.\n  return `root=$(realpath -- ${shellQuote(ROOT)}) && test \"$root\" = ${shellQuote(ROOT)} && ` +\n    `resolved=$(realpath -m -- ${shellQuote(candidate)}) && ` +\n    `case \"$resolved\" in \"$root\"|\"$root\"/*) ;; *) exit 1 ;; esac`;\n}\n\nexport 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) {","sourceCodeStart":1,"sourceCodeEnd":37,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/packages/plugins/sandbox-providers/createos/src/file-sync.ts#L1-L37","documentation":"After rejecting non-absolute/insecure paths, assertRemotePath normalizes the path and requires the result to be exactly /paperclip-workspace or inside /paperclip-workspace/. This catches paths that are syntactically absolute but resolve outside the sanctioned workspace root — the containment boundary for all CreateOS file transfers.","triggerScenarios":"Passing a path like /etc/passwd, /tmp/x, or /paperclip-workspace-sibling/file whose normalized form does not start with /paperclip-workspace/; also paths relying on normalization tricks that escape ROOT.","commonSituations":"Hardcoding destination paths meant for a different sandbox provider; writing to host-style temp directories; confusing the sandbox root with the host root; prefix string-check bugs when constructing paths (e.g. /paperclip-workspace-backup).","solutions":["Rewrite the destination so its normalized form is under /paperclip-workspace/ (or equals ROOT).","Use path.posix.join(ROOT, relative) instead of string concatenation to build remote paths.","Validate paths with assertRemotePath yourself in dev/test before shipping calls that transfer files.","Beware prefix bugs: /paperclip-workspace-evil is rejected; rely on the normalized startsWith(ROOT + '/') rule."],"exampleFix":"// before\nconst remote = \"/tmp/results/out.json\";\n// after\nconst remote = \"/paperclip-workspace/results/out.json\";","handlingStrategy":"validation","validationCode":"function underWorkspace(p) {\n  const n = path.posix.normalize(p);\n  return n === \"/paperclip-workspace\" || n.startsWith(\"/paperclip-workspace/\");\n}\n// assert before every transfer call\nif (!underWorkspace(remote)) throw new Error(\"destination must be inside /paperclip-workspace\");","typeGuard":"function isInsideWorkspace(p) {\n  const n = path.posix.normalize(p);\n  return n === \"/paperclip-workspace\" || n.startsWith(\"/paperclip-workspace/\");\n}","tryCatchPattern":"try {\n  await syncFiles(lease, transfers);\n} catch (e) {\n  if (e.message.includes(\"escapes the workspace\")) {\n    log.error(\"remote path outside /paperclip-workspace\", { transfers });\n  }\n  throw e;\n}","preventionTips":["Use path.posix.join(ROOT, relative) rather than string concatenation for destinations.","Run a dev-time assertion that all hardcoded remote paths normalize inside ROOT.","Remember prefix traps: /paperclip-workspace-* siblings are rejected."],"tags":["path-traversal","security","file-transfer","sandbox"],"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"}