TencentCloud/TencentDB-Agent-Memory · critical

Path traversal rejected: key "${key}" escapes rootDir

Error message

Path traversal rejected: key "${key}" escapes rootDir

What it means

resolvePath() resolves the storage key against rootDir and verifies the resulting absolute path stays inside rootDir. A key containing '..' segments that resolves outside the root — including a sibling directory whose name merely starts with rootDir's name — is rejected to prevent arbitrary file read/write (path traversal / directory escape). This is the core of the CR-6 security fix, since user-controllable fields (instanceId, sceneName, sessionKey) flow into keys.

Source

Thrown at MemoryCore/src/core/storage/local-backend.ts:84

      throw new Error("Storage key must not contain NUL character");
    }
    if (key.startsWith("/") || key.startsWith("\\")) {
      throw new Error(`Storage key must be relative, got absolute: ${key}`);
    }

    // Normalize key separators to OS path separators
    const normalized = key.split("/").join(sep);

    // Compute the absolute resolved path; resolve() collapses ".." segments.
    const absRoot = resolve(this.rootDir);
    const absResolved = resolve(absRoot, normalized);

    // Ensure the resolved path stays inside rootDir. Append sep so that
    // a key like "../rootDir2/foo" (which resolves to a sibling directory
    // whose name happens to start with rootDir's name) is also rejected.
    const rootWithSep = absRoot.endsWith(sep) ? absRoot : absRoot + sep;
    if (absResolved !== absRoot && !absResolved.startsWith(rootWithSep)) {
      throw new Error(`Path traversal rejected: key "${key}" escapes rootDir`);
    }

    return absResolved;
  }

  async putObject(key: string, content: string | Buffer, opts?: PutObjectOptions): Promise<void> {
    const filePath = this.resolvePath(key);
    await mkdir(dirname(filePath), { recursive: true });

    const buf = typeof content === "string" ? Buffer.from(content, "utf-8") : content;
    await writeFile(filePath, buf);

    // Store metadata as a sidecar .meta.json file if metadata is provided
    if (opts?.contentType || (opts?.metadata && Object.keys(opts.metadata).length > 0)) {
      const metaPath = filePath + ".meta.json";
      await writeFile(metaPath, JSON.stringify({
        contentType: opts.contentType,
        metadata: opts.metadata,

View on GitHub (pinned to 3efcd317b8)

Solutions

  1. Remove '..' segments from the key (or reject the input upstream) so it resolves inside rootDir.
  2. Sanitize/validate the user-controlled fields (instanceId, sceneName, sessionKey) before they are embedded into storage keys — allow only [A-Za-z0-9._-] and reject '..'.
  3. If data truly lives outside rootDir, point rootDir at the intended parent and use a relative key, rather than traversing with '..'.

Example fix

// before
await storage.getObject(`../../${instanceId}/memory.json`);
// after
if (instanceId.includes("..") || /[\\/]/.test(instanceId)) {
  throw new Error("invalid instanceId");
}
await storage.getObject(`${instanceId}/memory.json`);
Defensive patterns

Strategy: validation

Validate before calling

function keyStaysInsideRoot(key: string, rootDir: string): boolean {
  const path = require("node:path");
  const root = path.resolve(rootDir);
  const resolved = path.resolve(root, key);
  return resolved === root || resolved.startsWith(root + path.sep);
}
if (!keyStaysInsideRoot(key, rootDir)) throw new Error(`key escapes rootDir: ${key}`);

Type guard

function isTraversalSafeKey(key: unknown): key is string {
  return typeof key === "string" && key.length > 0 && !key.split("/").includes("..");
}

Try / catch

try {
  const data = await storage.getObject(key);
} catch (e) {
  if (e instanceof Error && e.message.startsWith("Path traversal rejected")) {
    logger.warn(`rejected traversal key: ${key}`); // treat as 400/403, never retry as-is
    return null;
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling putObject/getObject/etc. with a key containing '../' that escapes rootDir, e.g. '../../../etc/passwd' or '../rootDir2/foo'. Also a key that resolves exactly to a sibling like rootDir+'/../rootDir2/x'. Encoded or crafted user input (session ids with dots/dashes) concatenated into keys can produce this.

Common situations: User-supplied instance or session identifiers containing '..' reach the storage layer unvalidated; migration from a backend that silently collapsed '..'; an attacker probing standalone mode for arbitrary file access; tests using keys like '../fixture.json' expecting to reach a sibling directory.

Related errors


AI-assisted analysis of TencentCloud/TencentDB-Agent-Memory@3efcd317b8 (2026-09-01). Data as JSON: /api/errors/2e2b5172213f4443. Report an issue: GitHub.