{"record":{"id":"b41b517275014e06","repo":"paperclipai/paperclip","slug":"invalid-object-key-path","errorCode":null,"errorMessage":"Invalid object key path.","messagePattern":"Invalid object key path\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"cli/src/commands/worktree.ts","lineNumber":312,"sourceCode":"}\n\nfunction normalizeStorageObjectKey(objectKey: string): string {\n  const normalized = objectKey.replace(/\\\\/g, \"/\").trim();\n  if (!normalized || normalized.startsWith(\"/\")) {\n    throw new Error(\"Invalid object key.\");\n  }\n  const parts = normalized.split(\"/\").filter((part) => part.length > 0);\n  if (parts.length === 0 || parts.some((part) => part === \".\" || part === \"..\")) {\n    throw new Error(\"Invalid object key.\");\n  }\n  return parts.join(\"/\");\n}\n\nfunction resolveLocalStoragePath(baseDir: string, objectKey: string): string {\n  const resolved = path.resolve(baseDir, normalizeStorageObjectKey(objectKey));\n  const root = path.resolve(baseDir);\n  if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) {\n    throw new Error(\"Invalid object key path.\");\n  }\n  return resolved;\n}\n\nasync function s3BodyToBuffer(body: unknown): Promise<Buffer> {\n  if (!body) {\n    throw new Error(\"Object not found.\");\n  }\n  if (Buffer.isBuffer(body)) {\n    return body;\n  }\n  if (body instanceof Readable) {\n    return await streamToBuffer(body);\n  }\n\n  const candidate = body as {\n    transformToWebStream?: () => ReadableStream<Uint8Array>;\n    arrayBuffer?: () => Promise<ArrayBuffer>;","sourceCodeStart":294,"sourceCodeEnd":330,"githubUrl":"https://github.com/paperclipai/paperclip/blob/67001ec6eb96ae601aa27bc91d9b2415d665334a/cli/src/commands/worktree.ts#L294-L330","documentation":"Thrown by resolveLocalStoragePath when an object key, after normalization and path.resolve against baseDir, escapes the storage root directory. This is a path-traversal guard ensuring local_disk storage writes/reads stay confined under the configured baseDir. It fires only when the resolved path is neither exactly the root nor a child of root plus a path separator.","triggerScenarios":"Calling ConfiguredStorage.getObject/putObject on a local_disk provider with an objectKey whose normalized form resolves outside baseDir (e.g. an absolute path on another drive, or a key that after normalizeStorageObjectKey still resolves upward via symlinks). The earlier normalizeStorageObjectKey blocks literal '..' segments, so this is the second-line defense for OS-level path resolution discrepancies (Windows drive letters, case-insensitive roots, symlinked baseDir).","commonSituations":"baseDir configured as a relative or symlinked path that does not match its realpath; cross-platform path separators on Windows; object keys containing drive letters or UNC paths that slip past the string-based checks; baseDir moved or renamed after config write.","solutions":["Ensure config.storage.localDisk.baseDir is an absolute, canonical path (no symlinks) matching the runtime filesystem layout.","Sanitize objectKey with path.normalize and strip leading separators before passing it to getObject/putObject.","Verify the companyId prefix and reject any objectKey containing absolute paths or alternate roots before calling storage.","If baseDir is symlinked, resolve it to realpath once at config load and reuse the canonical form."],"exampleFix":"// before\nconst baseDir = config.storage.localDisk.baseDir;\n// after\nconst baseDir = fs.realpathSync(expandHomePrefix(config.storage.localDisk.baseDir));","handlingStrategy":"validation","validationCode":"function isSafeObjectKey(baseDir: string, objectKey: string): boolean {\n  try {\n    const root = fs.realpathSync(path.resolve(baseDir));\n    const resolved = path.resolve(root, objectKey.replace(/\\\\/g, '/'));\n    return resolved === root || resolved.startsWith(root + path.sep);\n  } catch {\n    return false;\n  }\n}\n// before storage.getObject/putObject:\nif (!isSafeObjectKey(baseDir, key)) throw new Error('Refusing unsafe object key');","typeGuard":"function isConfinedPath(root: string, target: string): boolean {\n  const r = path.resolve(root);\n  const t = path.resolve(target);\n  return t === r || t.startsWith(r + path.sep);\n}","tryCatchPattern":"try {\n  await storage.putObject(companyId, key, buf, ct);\n} catch (e) {\n  if (e instanceof Error && e.message === 'Invalid object key path.') {\n    // reject the key client-side, log security event\n  } else throw e;\n}","preventionTips":["Canonicalize baseDir with realpathSync at config load.","Reject object keys containing absolute paths, drive letters, or UNC prefixes before storage calls.","Unit-test path traversal vectors (.., %2e%2e, symlinks) against the storage layer."],"tags":["path-traversal","storage","local-disk","security","filesystem"],"backgroundTag":null,"analyzedSha":"67001ec6eb96ae601aa27bc91d9b2415d665334a","analyzedAt":"2026-08-12T12:05:45.408Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}