paperclipai/paperclip · error · Error

Invalid object key path.

Error message

Invalid object key path.

What it means

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.

Source

Thrown at cli/src/commands/worktree.ts:312

}

function normalizeStorageObjectKey(objectKey: string): string {
  const normalized = objectKey.replace(/\\/g, "/").trim();
  if (!normalized || normalized.startsWith("/")) {
    throw new Error("Invalid object key.");
  }
  const parts = normalized.split("/").filter((part) => part.length > 0);
  if (parts.length === 0 || parts.some((part) => part === "." || part === "..")) {
    throw new Error("Invalid object key.");
  }
  return parts.join("/");
}

function resolveLocalStoragePath(baseDir: string, objectKey: string): string {
  const resolved = path.resolve(baseDir, normalizeStorageObjectKey(objectKey));
  const root = path.resolve(baseDir);
  if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) {
    throw new Error("Invalid object key path.");
  }
  return resolved;
}

async function s3BodyToBuffer(body: unknown): Promise<Buffer> {
  if (!body) {
    throw new Error("Object not found.");
  }
  if (Buffer.isBuffer(body)) {
    return body;
  }
  if (body instanceof Readable) {
    return await streamToBuffer(body);
  }

  const candidate = body as {
    transformToWebStream?: () => ReadableStream<Uint8Array>;
    arrayBuffer?: () => Promise<ArrayBuffer>;

View on GitHub (pinned to 67001ec6eb)

Solutions

  1. Ensure config.storage.localDisk.baseDir is an absolute, canonical path (no symlinks) matching the runtime filesystem layout.
  2. Sanitize objectKey with path.normalize and strip leading separators before passing it to getObject/putObject.
  3. Verify the companyId prefix and reject any objectKey containing absolute paths or alternate roots before calling storage.
  4. If baseDir is symlinked, resolve it to realpath once at config load and reuse the canonical form.

Example fix

// before
const baseDir = config.storage.localDisk.baseDir;
// after
const baseDir = fs.realpathSync(expandHomePrefix(config.storage.localDisk.baseDir));
Defensive patterns

Strategy: validation

Validate before calling

function isSafeObjectKey(baseDir: string, objectKey: string): boolean {
  try {
    const root = fs.realpathSync(path.resolve(baseDir));
    const resolved = path.resolve(root, objectKey.replace(/\\/g, '/'));
    return resolved === root || resolved.startsWith(root + path.sep);
  } catch {
    return false;
  }
}
// before storage.getObject/putObject:
if (!isSafeObjectKey(baseDir, key)) throw new Error('Refusing unsafe object key');

Type guard

function isConfinedPath(root: string, target: string): boolean {
  const r = path.resolve(root);
  const t = path.resolve(target);
  return t === r || t.startsWith(r + path.sep);
}

Try / catch

try {
  await storage.putObject(companyId, key, buf, ct);
} catch (e) {
  if (e instanceof Error && e.message === 'Invalid object key path.') {
    // reject the key client-side, log security event
  } else throw e;
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12). Data as JSON: /api/errors/b41b517275014e06. Report an issue: GitHub.