{"record":{"id":"2e2b5172213f4443","repo":"TencentCloud/TencentDB-Agent-Memory","slug":"path-traversal-rejected-key-key-escapes-root","errorCode":null,"errorMessage":"Path traversal rejected: key \"${key}\" escapes rootDir","messagePattern":"Path traversal rejected: key \"(.+?)\" escapes rootDir","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"MemoryCore/src/core/storage/local-backend.ts","lineNumber":84,"sourceCode":"      throw new Error(\"Storage key must not contain NUL character\");\n    }\n    if (key.startsWith(\"/\") || key.startsWith(\"\\\\\")) {\n      throw new Error(`Storage key must be relative, got absolute: ${key}`);\n    }\n\n    // Normalize key separators to OS path separators\n    const normalized = key.split(\"/\").join(sep);\n\n    // Compute the absolute resolved path; resolve() collapses \"..\" segments.\n    const absRoot = resolve(this.rootDir);\n    const absResolved = resolve(absRoot, normalized);\n\n    // Ensure the resolved path stays inside rootDir. Append sep so that\n    // a key like \"../rootDir2/foo\" (which resolves to a sibling directory\n    // whose name happens to start with rootDir's name) is also rejected.\n    const rootWithSep = absRoot.endsWith(sep) ? absRoot : absRoot + sep;\n    if (absResolved !== absRoot && !absResolved.startsWith(rootWithSep)) {\n      throw new Error(`Path traversal rejected: key \"${key}\" escapes rootDir`);\n    }\n\n    return absResolved;\n  }\n\n  async putObject(key: string, content: string | Buffer, opts?: PutObjectOptions): Promise<void> {\n    const filePath = this.resolvePath(key);\n    await mkdir(dirname(filePath), { recursive: true });\n\n    const buf = typeof content === \"string\" ? Buffer.from(content, \"utf-8\") : content;\n    await writeFile(filePath, buf);\n\n    // Store metadata as a sidecar .meta.json file if metadata is provided\n    if (opts?.contentType || (opts?.metadata && Object.keys(opts.metadata).length > 0)) {\n      const metaPath = filePath + \".meta.json\";\n      await writeFile(metaPath, JSON.stringify({\n        contentType: opts.contentType,\n        metadata: opts.metadata,","sourceCodeStart":66,"sourceCodeEnd":102,"githubUrl":"https://github.com/TencentCloud/TencentDB-Agent-Memory/blob/3efcd317b84146d6a08518ac0f7ee7c8a8d200ec/MemoryCore/src/core/storage/local-backend.ts#L66-L102","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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 '..'."],"exampleFix":"// before\nawait storage.getObject(`../../${instanceId}/memory.json`);\n// after\nif (instanceId.includes(\"..\") || /[\\\\/]/.test(instanceId)) {\n  throw new Error(\"invalid instanceId\");\n}\nawait storage.getObject(`${instanceId}/memory.json`);","handlingStrategy":"validation","validationCode":"function keyStaysInsideRoot(key: string, rootDir: string): boolean {\n  const path = require(\"node:path\");\n  const root = path.resolve(rootDir);\n  const resolved = path.resolve(root, key);\n  return resolved === root || resolved.startsWith(root + path.sep);\n}\nif (!keyStaysInsideRoot(key, rootDir)) throw new Error(`key escapes rootDir: ${key}`);","typeGuard":"function isTraversalSafeKey(key: unknown): key is string {\n  return typeof key === \"string\" && key.length > 0 && !key.split(\"/\").includes(\"..\");\n}","tryCatchPattern":"try {\n  const data = await storage.getObject(key);\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith(\"Path traversal rejected\")) {\n    logger.warn(`rejected traversal key: ${key}`); // treat as 400/403, never retry as-is\n    return null;\n  }\n  throw e;\n}","preventionTips":["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')."],"tags":["path-traversal","security","validation","storage"],"backgroundTag":"path-traversal-attempt","analyzedSha":"3efcd317b84146d6a08518ac0f7ee7c8a8d200ec","analyzedAt":"2026-09-01T05:44:22.276Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}