T02 · Agent Memory Poisoning
- Location
- index.js:86
- Finding
- Persistent Agent Memory Poisoning Through Untrusted Conversation Content<![CDATA[ ## Vulnerability Details **File Location**: `index.js`, lines 86-130 and 139-159 **Vulnerability Type**: Persistent memory poisoning through indirect prompt injection **Risk Level**: High ### Vulnerable Code ```js // Format conversation for LLM const conversation = messages .filter(m => m.content && m.content.trim().length > 0) .map(m => `**${m.role}**: ${m.content}`) .join('\n\n'); // Call LLM to extract key points const prompt = getExtractionPrompt(conversation); const result = await ctx.invokeLLM(prompt); // Parse result let extracted; try { // Try to extract JSON from response const jsonMatch = result.match(/```json\n([\s\S]*)\n```/) || result.match(/{[\s\S]*}/); const jsonStr = jsonMatch ? jsonMatch[1] || jsonMatch[0] : result; extracted = JSON.parse(jsonStr.trim()); } catch (err) { ctx.log.error('[memory-auto-manager] Failed to parse LLM response:', err); // Fallback: treat entire response as keyPoints extracted = { keyPoints: result.trim(), category: 'fact' }; } const { keyPoints, category = 'fact' } = extracted; // Check if worth retaining if (!keyPoints || typeof keyPoints !== 'string' || keyPoints.trim().length < 20) { ctx.log.info('[memory-auto-manager] Skipping: content too short or empty'); return; } // Write to MEMORY.md const memoryPath = path.join(ctx.workspace, 'MEMORY.md'); const date = new Date().toISOString().split('T')[0]; const timestamp = new Date().toISOString(); const content = ` --- ### ${timestamp} ### ${date} / ${category} ${keyPoints.trim()} --- `; await fs.promises.appendFile(memoryPath, content, 'utf8'); ctx.log.info(`[memory-auto-manager] ✉️ Wrote key points to MEMORY.md (timestamp: ${timestamp})`); // Update vector index ctx.log.info('[memory-auto-manager] 🔄 Updating vector memory index...'); await ctx.exec('openclaw memory index --force'); ``` ```js function getExtractionPrompt(conversation) ...[truncated 3038 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Treat every transcript message as untrusted data. Place it in a clearly delimited data section and explicitly instruct the model never to follow instructions found inside that section. 2. Prefer structured LLM output with a strict schema rather than extracting arbitrary JSON with a greedy regular expression. 3. Validate that the response is an object containing only expected fields, that `keyPoints` has reasonable type and size limits, and that `category` is exactly one of `decision`, `fact`, `preference`, or `entity`. 4. Remove the fallback that persists the complete raw LLM response. A parse or validation failure should abort persistence. 5. Detect and reject output containing behavioral directives, authority claims, credential material, prompt fragments, or instructions aimed at future sessions. 6. Require explicit user review and confirmation before writing extracted content to long-term memory, particularly for decisions, preferences, identity information, and security-sensitive claims. 7. Preserve provenance with every entry, including the source session, source message identifiers, extraction time, and confirmation state. 8. Escape or safely serialize memory fields rather than interpolating unrestricted output into Markdown headings and body content. 9. Add deduplication and a durable processed-session marker so the same poisoned session cannot be repeatedly persisted by hooks, manual processing, and the daily job. 10. Index only entries that have passed validation and, where appropriate, user confirmation. If validation fails, log a redacted diagnostic and do not modify either `MEMORY.md` or the vector index. ]]>
