{"record":{"id":"3cf4948d42f9e766","repo":"paperclipai/paperclip","slug":"sandbox-runtime-asset-key-is-not-a-simple-path-seg","errorCode":null,"errorMessage":"sandbox runtime asset key is not a simple path segment: ${key}","messagePattern":"sandbox runtime asset key is not a simple path segment: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/adapter-utils/src/sandbox-managed-runtime.ts","lineNumber":433,"sourceCode":"}\n\n// The workspace stages under `<runtimeRootDir>/workspace-upload.tar` and, for a\n// git-backed workspace, under `<runtimeRootDir>/git-workspace-upload.tar`. Each\n// asset stages under `<runtimeRootDir>/<key>-upload.tar`, so an asset key equal to\n// one of these stems resolves to the same remote archive path. Reserve the stems.\nconst RESERVED_RUNTIME_ASSET_KEYS = new Set([\"workspace\", \"git-workspace\"]);\n\n// Reject an asset key before any path is built from it. An asset key becomes a\n// remote directory (`<runtimeRootDir>/<key>`), a remote archive name\n// (`<key>-upload.tar`), and a host temp file (`<key>.tar`). A path separator or\n// `..` in the key escapes those roots. A reserved stem makes the asset archive\n// share a path with the workspace archive; under concurrent sync the asset task\n// and the workspace task then write or upload the same archive at the same time,\n// which fails extraction nondeterministically or puts asset bytes in the\n// workspace. Fail closed on both cases.\nfunction assertRuntimeAssetKeyIsSafe(key: string): void {\n  if (key.length === 0 || key.includes(\"/\") || key.includes(\"\\\\\") || key.includes(\"..\")) {\n    throw new Error(`sandbox runtime asset key is not a simple path segment: ${key}`);\n  }\n  if (RESERVED_RUNTIME_ASSET_KEYS.has(key)) {\n    throw new Error(`sandbox runtime asset key collides with a reserved runtime archive name: ${key}`);\n  }\n}\n\nexport function parseSandboxRemoteExecutionSpec(value: unknown): SandboxRemoteExecutionSpec | null {\n  const parsed = asObject(value);\n  const transport = asString(parsed.transport).trim();\n  const provider = asString(parsed.provider).trim();\n  const sandboxId = asString(parsed.sandboxId).trim();\n  const remoteCwd = asString(parsed.remoteCwd).trim();\n  const timeoutMs = asNumber(parsed.timeoutMs);\n\n  if (\n    transport !== \"sandbox\" ||\n    provider.length === 0 ||\n    sandboxId.length === 0 ||","sourceCodeStart":415,"sourceCodeEnd":451,"githubUrl":"https://github.com/paperclipai/paperclip/blob/a7e689b3c35347b529cb9f54c9b9a8575a3dcab6/packages/adapter-utils/src/sandbox-managed-runtime.ts#L415-L451","documentation":"Thrown by assertRuntimeAssetKeyIsSafe (called from the sandbox managed runtime before building any paths, sandbox-managed-runtime.ts:774) when a runtime asset key is empty or contains \"/\", \"\\\\\", or \"..\". The key becomes a remote directory, an archive name, and a host temp file, so a separator or traversal sequence could escape those roots; the runtime fails closed instead of building the path.","triggerScenarios":"A SandboxRemoteExecutionSpec's runtimeAssets entry uses a key derived from a file path (\"cache/node_modules\"), a Windows path (\"assets\\\\bin\"), a traversal (\"../secret\"), or an empty string; path building is rejected before any sync starts.","commonSituations":"Generating keys from user input or workspace-relative paths without normalizing; accepting asset manifests from config files or APIs where callers paste paths instead of names; Windows-developed manifests leaking backslash separators.","solutions":["Use a flat, single-segment key: letters, digits, dot, dash, underscore (e.g. \"build-cache\", \"node_modules-cache\").","When deriving a key from a path, take path.basename() and strip anything outside a safe alphabet.","Reject empty keys and any key containing \"/\", \"\\\\\", or \"..\" at the boundary where the asset list enters your system.","Keep multi-level layouts inside the asset archive, not in the key."],"exampleFix":"// before\nconst key = userInputPath; // \"tools/../workspace\"\n\n// after\nconst key = sanitizeAssetKey(userInputPath);\nfunction sanitizeAssetKey(raw: string): string {\n  const key = path.basename(raw.trim());\n  if (!key || key.includes(\"..\") || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(key)) {\n    throw new Error(`invalid runtime asset key: ${JSON.stringify(raw)}`);\n  }\n  return key;\n}","handlingStrategy":"validation","validationCode":"const RUNTIME_ASSET_KEY_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;\nfunction isSafeRuntimeAssetKey(key: string): boolean {\n  return key.length > 0 && !key.includes(\"/\") && !key.includes(\"\\\\\") &&\n    !key.includes(\"..\") && RUNTIME_ASSET_KEY_RE.test(key);\n}\nconst assets = manifest.assets.filter((a) => isSafeRuntimeAssetKey(a.key));","typeGuard":"function isSafeRuntimeAssetKey(key: unknown): key is string {\n  return typeof key === \"string\" && key.length > 0 &&\n    !key.includes(\"/\") && !key.includes(\"\\\\\") && !key.includes(\"..\");\n}","tryCatchPattern":"try {\n  buildRuntimeAssets(assets);\n} catch (error) {\n  if (error instanceof Error && error.message.includes(\"not a simple path segment\")) {\n    return rejectManifest(error); // surface which key is bad; do not sanitize silently\n  }\n  throw error;\n}","preventionTips":["Generate keys from path.basename() plus an allowlist alphabet.","Validate asset manifests at ingestion (API/config boundary), not at sync time.","Never accept raw filesystem paths or user strings as asset keys."],"tags":["sandbox","path-traversal","validation","security","assets"],"backgroundTag":"path-traversal-rejected","analyzedSha":"a7e689b3c35347b529cb9f54c9b9a8575a3dcab6","analyzedAt":"2026-08-21T17:58:32.592Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}