{"record":{"id":"cff0c5c9581789f5","repo":"paperclipai/paperclip","slug":"additional-source-projectid-is-not-a-simple-path-s","errorCode":null,"errorMessage":"additional source projectId is not a simple path segment: ${projectId}","messagePattern":"additional source projectId is not a simple path segment: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/adapter-utils/src/remote-managed-runtime.ts","lineNumber":193,"sourceCode":"  // Stage each referenced (additional) project as a plain, read-only tree in its\n  // OWN isolated remote directory (`project-<projectId>`). Additional sources\n  // never get the anchor's git-history/overlay semantics. Per-project failure\n  // isolation: one project's failure logs a warning and is skipped; the run and\n  // the other projects continue (no workspace restore, unlike an asset failure).\n  const additionalSourceDirs: Record<string, string> = {};\n  for (const source of input.additionalSources ?? []) {\n    const { localPath, projectId } = source;\n    try {\n      if (!path.posix.isAbsolute(localPath)) {\n        throw new Error(`additional source localPath is not an absolute path: ${localPath}`);\n      }\n      if (\n        projectId.length === 0 ||\n        projectId.includes(\"/\") ||\n        projectId.includes(\"\\\\\") ||\n        projectId.includes(\"..\")\n      ) {\n        throw new Error(`additional source projectId is not a simple path segment: ${projectId}`);\n      }\n      const remoteDir = path.posix.join(runtimeRootDir, `project-${projectId}`);\n      await syncDirectoryToSsh({\n        spec: input.spec,\n        localDir: localPath,\n        remoteDir,\n        exclude: REMOTE_ADDITIONAL_SOURCE_HEAVY_DIR_EXCLUDES,\n        onProgress: input.onProgress,\n        progressLabel: `project-${projectId}`,\n      });\n      additionalSourceDirs[projectId] = remoteDir;\n    } catch (error) {\n      console.warn(\n        `[paperclip] Failed to stage referenced project ${projectId}; skipping it. ${String(error)}`,\n      );\n    }\n  }\n","sourceCodeStart":175,"sourceCodeEnd":211,"githubUrl":"https://github.com/paperclipai/paperclip/blob/67001ec6eb96ae601aa27bc91d9b2415d665334a/packages/adapter-utils/src/remote-managed-runtime.ts#L175-L211","documentation":"Thrown inside the additional-sources staging loop when projectId is empty or contains \"/\", \"\\\\\", or \"..\". The projectId is interpolated into a remote path (path.posix.join(runtimeRootDir, `project-${projectId}`)), so any path-like content would let a malicious or careless value escape the per-project directory or traverse the remote filesystem. This is a path-traversal guard.","triggerScenarios":"additionalSources: [{ localPath: \"/abs/path\", projectId: \"\" }], [{ ..., projectId: \"../etc\" }], [{ ..., projectId: \"foo/bar\" }], or [{ ..., projectId: \"foo\\\\..\" }]. Each is rejected at remote-managed-runtime.ts:187-194. Like 334, the throw is caught per-source and logged as a warning; the source is skipped, not fatal.","commonSituations":"User-supplied project identifiers that include slashes (org/repo slugs from GitHub), project IDs derived from filesystem paths, empty IDs from incomplete form submissions, or untrusted upstream payloads. The guard prevents these from being concatenated into the remote rsync target.","solutions":["Use a simple opaque identifier for projectId: alphanumeric, hyphen, underscore — no path separators or dot segments.","If your IDs are org/repo slugs, sanitize them: slug.replace(/[\\\\/]+/g, \"-\").","Reject empty projectIds at the config boundary; default to a stable hash or UUID if the caller does not provide one.","Validate projectId against /^[A-Za-z0-9_-]+$/ before staging to fail fast with a clear upstream message."],"exampleFix":"// before\nadditionalSources: [\n  { localPath: \"/srv/foo\", projectId: \"org/foo\" },\n  { localPath: \"/srv/bar\", projectId: \"\" },\n]\n\n// after\nadditionalSources: [\n  { localPath: \"/srv/foo\", projectId: \"org-foo\" },\n  { localPath: \"/srv/bar\", projectId: \"bar\" },\n]","handlingStrategy":"validation","validationCode":"const PROJECT_ID_RE = /^[A-Za-z0-9_-]+$/;\nfunction assertProjectId(id: string): void {\n  if (!PROJECT_ID_RE.test(id)) {\n    throw new Error(`projectId must be alphanumeric/hyphen/underscore (got ${id}); slashes and .. are rejected as path traversal`);\n  }\n}\n\nfor (const source of input.additionalSources ?? []) assertProjectId(source.projectId);","typeGuard":"function isSafeProjectId(value: unknown): value is string {\n  return typeof value === \"string\" && value.length > 0 && !value.includes(\"/\") && !value.includes(\"\\\\\") && !value.includes(\"..\") && /^[A-Za-z0-9_-]+$/.test(value);\n}","tryCatchPattern":"// Per-source failures are already caught and logged as warnings by remote-managed-runtime.\n// To make failures loud, pre-validate before staging:\nfor (const source of input.additionalSources ?? []) {\n  if (!isSafeProjectId(source.projectId)) {\n    throw new Error(`Unsafe projectId rejected: ${source.projectId}`);\n  }\n}","preventionTips":["Use opaque alphanumeric project IDs; never put org/repo slugs in projectId.","Sanitize slugs at the config boundary: slug.replace(/[\\\\/]+/g, \"-\").","Reject empty projectIds upstream; default to a stable hash/UUID.","Validate against /^[A-Za-z0-9_-]+$/ at schema load."],"tags":["remote-runtime","paths","security","path-traversal","config-validation"],"backgroundTag":null,"analyzedSha":"67001ec6eb96ae601aa27bc91d9b2415d665334a","analyzedAt":"2026-08-12T12:05:45.408Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}