T02 · Agent Memory Poisoning
Error
- Location
- convex/memory.js:6
- Finding
- Unauthenticated Public Functions Allow Cross-Agent Memory and Daily-Log Access<![CDATA[ ## Vulnerability Details **File Location**: `convex/memory.js:6-52`, `convex/memory.js:68-109`; `convex/components/openclawBackend/memory.js:5-92`, `convex/components/openclawBackend/memory.js:107-197` **Vulnerability Type**: Broken access control, cross-tenant data access, and persistent memory poisoning **Risk Level**: High ### Vulnerable Code ```js export const addMemory = mutation({ args: { agentId: v.string(), type: v.union( v.literal("fact"), v.literal("preference"), v.literal("decision"), v.literal("note"), ), content: v.string(), tags: v.optional(v.array(v.string())), }, returns: v.string(), handler: async (ctx, args) => { return await ctx.runMutation(components.openclawBackend.memory.addMemory, args); }, }); export const searchMemory = query({ args: { agentId: v.string(), type: v.optional( v.union( v.literal("fact"), v.literal("preference"), v.literal("decision"), v.literal("note"), ), ), limit: v.optional(v.number()), }, returns: v.array( v.object({ _id: v.string(), type: v.union( v.literal("fact"), v.literal("preference"), v.literal("decision"), v.literal("note"), ), content: v.string(), tags: v.optional(v.array(v.string())), createdAt: v.number(), }), ), handler: async (ctx, args) => { return await ctx.runQuery(components.openclawBackend.memory.searchMemory, args); }, }); ``` The same caller-controlled identity pattern is used by the daily-log functions: ```js export const writeDailyLog = mutation({ args: { agentId: v.string(), date: v.string(), content: v.string(), }, returns: v.string(), handler: async (ctx, args) => { return await ctx.runMutation(components.openclawBackend.memory.writeDailyLog, args); }, }); export const getDailyLog = query({ args: { agentId: v.string(), date: v.string(), ...[truncated 3413 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Require authentication in every public query and mutation: ```js const identity = await ctx.auth.getUserIdentity(); if (!identity) { throw new Error("Unauthenticated"); } ``` 2. Do not use a caller-supplied `agentId` as proof of ownership. Derive the tenant and permitted agent scope from the authenticated identity or a server-maintained authorization mapping. 3. If callers must provide an `agentId`, verify that the authenticated principal is explicitly authorized to access that agent before forwarding the request. 4. Store a tenant or owner identifier with each memory and daily-log record and include it in indexes and authorization checks. 5. Apply the same authorization policy to add, search, delete, write, get, and list operations. 6. Keep component functions internal where possible and expose only authenticated root wrappers. 7. Treat retrieved memory and log content as untrusted data. Delimit it from system instructions and ensure the agent is explicitly instructed not to execute directives found in stored content. 8. Add tests proving that one authenticated principal cannot read or modify another principal's records and that unauthenticated calls are rejected. ]]>
