{"record":{"id":"1376fe8affc4cc09","repo":"TencentCloud/TencentDB-Agent-Memory","slug":"storage-key-must-be-relative-got-absolute-key","errorCode":null,"errorMessage":"Storage key must be relative, got absolute: ${key}","messagePattern":"Storage key must be relative, got absolute: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"MemoryCore/src/core/storage/local-backend.ts","lineNumber":69,"sourceCode":"   * rootDir). Affects standalone mode where user-controllable fields like\n   * instanceId / sceneName / sessionKey end up in the key.\n   *\n   * Rejected:\n   * - Empty key\n   * - Keys containing NUL (\\0) — POSIX/Linux path terminator, can confuse\n   *   downstream tooling (sqlite, file managers).\n   * - Keys with leading \"/\" or \"\\\" (absolute paths).\n   * - Keys whose resolved path falls outside rootDir (../ traversal).\n   */\n  private resolvePath(key: string): string {\n    if (!key || typeof key !== \"string\") {\n      throw new Error(`Invalid storage key: ${JSON.stringify(key)}`);\n    }\n    if (key.includes(\"\\0\")) {\n      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;","sourceCodeStart":51,"sourceCodeEnd":87,"githubUrl":"https://github.com/TencentCloud/TencentDB-Agent-Memory/blob/3efcd317b84146d6a08518ac0f7ee7c8a8d200ec/MemoryCore/src/core/storage/local-backend.ts#L51-L87","documentation":"LocalStorageBackend.resolvePath() maps storage object keys to file paths under a configured rootDir. As part of a path-traversal security fix (CR-6), it rejects any key that starts with '/' or '\\' because such keys are absolute paths and could target arbitrary filesystem locations instead of staying inside rootDir. Keys must be relative path segments joined under the backend root.","triggerScenarios":"Calling putObject/getObject/appendObject/list (any method that calls resolvePath) with a key that begins with '/' (POSIX absolute, e.g. '/etc/passwd') or '\\\\' (Windows absolute, e.g. '\\\\\\\\server\\\\share\\\\file'). Commonly produced by joining user-controlled fields like instanceId, sceneName or sessionKey that already contain a leading slash into the key template.","commonSituations":"A config value such as sessionKey or instanceId was set to an absolute path by mistake; code migrated from an API that accepted absolute paths; string concatenation like root + '/' + key where key already starts with '/'; on Windows, UNC paths ('\\\\\\\\server\\\\share') pasted into a config field.","solutions":["Strip leading '/' or '\\\\' (and collapse duplicate separators) from the key before passing it to the backend.","Fix the source of the key (config or user input) so it is stored as a relative path segment; validate at ingestion.","If an absolute path is genuinely needed, configure the backend's rootDir to that location and use a relative key instead."],"exampleFix":"// before\nconst key = `${sessionKey}/memory.json`; // sessionKey = \"/data/instances/abc\"\nawait storage.putObject(key, data);\n// after\nconst relKey = sessionKey.replace(/^[\\\\/]+/, \"\");\nconst key = `${relKey}/memory.json`;\nawait storage.putObject(key, data);","handlingStrategy":"validation","validationCode":"function isSafeRelativeKey(key: string): boolean {\n  return typeof key === \"string\" && key.length > 0 && !key.startsWith(\"/\") && !key.startsWith(\"\\\\\") && !key.includes(\"\\0\");\n}\nif (!isSafeRelativeKey(key)) throw new Error(`key must be relative: ${key}`);","typeGuard":"function isRelativeKey(key: unknown): key is string {\n  return typeof key === \"string\" && key.length > 0 && !key.startsWith(\"/\") && !key.startsWith(\"\\\\\");\n}","tryCatchPattern":"try {\n  await storage.putObject(key, data);\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith(\"Storage key must be relative\")) {\n    key = key.replace(/^[\\\\/]+/, \"\");\n    await storage.putObject(key, data);\n  } else throw e;\n}","preventionTips":["Always build keys from sanitized relative segments; strip leading slashes from user input at ingestion.","Validate config fields like sessionKey/instanceId against /^[^\\\\/]/ before they reach storage keys.","Add a unit test asserting keys beginning with '/' or '\\\\' are rejected."],"tags":["path-traversal","validation","storage","security"],"backgroundTag":"absolute-path-storage-key-rejected","analyzedSha":"3efcd317b84146d6a08518ac0f7ee7c8a8d200ec","analyzedAt":"2026-09-01T05:44:22.276Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}