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
- Remove '..' segments from the key (or reject the input upstream) so it resolves inside rootDir.
- Sanitize/validate the user-controlled fields (instanceId, sceneName, sessionKey) before they are embedded into storage keys — allow only [A-Za-z0-9._-] and reject '..'.
- 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
- Treat any traversal-rejection as a potential security probe: log it and never echo the key into responses.
- Restrict user-controlled key components to a whitelist like /^[A-Za-z0-9._-]+$/ and explicitly reject '..'.
- Keep path.resolve-based containment checks in your own code before handing keys to any storage backend.
- Add tests for keys like '../x', 'a/../../x' and sibling-prefix cases ('../rootDir2/foo').
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
- Path traversal rejected in scoped storage key: ${key}
- Storage key must be relative, got absolute: ${key}
- Invalid generation log key
- invalid team_id for template path: ${teamId}
- Generation log object key exceeds COS limit
AI-assisted analysis of TencentCloud/TencentDB-Agent-Memory@3efcd317b8 (2026-09-01).
Data as JSON: /api/errors/2e2b5172213f4443.
Report an issue: GitHub.