T02 · Agent Memory Poisoning
Error
- Location
- index.js:54
- Finding
- Persistent Agent Memory Poisoning Through Unsanitized DingTalk Metadata## Vulnerability Details **File Location**: `index.js:54-80, 92-141` **Vulnerability Type**: Persistent injection of attacker-controlled content into agent memory **Risk Level**: High ### Vulnerable Code ```js function saveGroup(conversationId, groupName) { const data = readMemory(); const now = new Date().toISOString(); // Find an existing record let group = data.groups.find(g => g.conversationId === conversationId); if (group) { group.lastActiveAt = now; group.mentionCount = (group.mentionCount || 0) + 1; if (groupName && !group.groupName) { group.groupName = groupName; } } else { group = { conversationId, groupName: groupName || 'Unknown group name', firstSeenAt: now, lastActiveAt: now, mentionCount: 1 }; data.groups.push(group); } writeMemory(data); // Synchronize the data into MEMORY.md updateMemoryMd(data.groups); return group; } ``` ```js function updateMemoryMd(groups) { if (!fs.existsSync(MEMORY_MD_FILE)) { console.log('MEMORY.md does not exist; skipping update'); return; } let content = fs.readFileSync(MEMORY_MD_FILE, 'utf-8'); const tableLines = [ '| # | Group Name | Group ID | Purpose |', '|---|------------|----------|---------|' ]; groups.forEach((g, index) => { const usage = getUsageByGroupId(g.conversationId); tableLines.push( `| ${index + 1} | ${g.groupName} | \`${g.conversationId}\` | ${usage} |` ); }); // ... fs.writeFileSync(MEMORY_MD_FILE, content, 'utf-8'); } ``` ### Technical Analysis The Skill accepts `groupName` and `conversationId` from a DingTalk group event and stores them without validation. It subsequently interpolates both values directly into `~/.openclaw/workspace/MEMORY.md`. Neither value is constrained to an expected format or maximum length. Markdown-sensitive characters—including line breaks, pipes, and backticks—are not removed or escaped. An attacker-contro ...[truncated 2496 chars]
- Remediation
- ## Remediation Suggestions 1. **Validate the conversation ID** - Require the exact syntax used by legitimate DingTalk conversation identifiers. - Enforce a conservative maximum length. - Reject control characters, whitespace, line breaks, backticks, and Markdown delimiters. - Perform validation at the start of `saveGroup()` before any persistence occurs. 2. **Normalize and constrain group names** - Remove carriage returns, newlines, null bytes, and other control characters. - Enforce a reasonable maximum length. - Reject or replace unexpected Unicode control and bidirectional formatting characters. 3. **Escape all Markdown output** - Escape `|` as `\|`. - Escape backticks or render values without inline-code delimiters. - Convert line breaks to spaces before creating table rows. - Apply escaping to every externally influenced field, even after validation. 4. **Separate untrusted records from agent instructions** - Prefer retaining remote metadata only in structured JSON. - Do not place raw remote values in a file automatically loaded as trusted agent memory. - If a Markdown summary is required, clearly mark generated values as untrusted display data and generate it from sanitized fields only. 5. **Prevent persistent reinjection** - Validate and sanitize existing JSON records before generating `MEMORY.md`. - Provide a migration routine that quarantines malformed historical records. - Correct both the Markdown file and the JSON source when removing malicious entries. 6. **Harden file updates** - Generate the updated document from a well-defined section boundary rather than relying on broad substring operations. - Write through a temporary file followed by an atomic rename to reduce corruption risk. - Set restrictive file permissions appropriate for long-term agent state. A safe rendering helper should enforce type, length, single-line output, and Markdown escaping before interpolation: ```js function ...[truncated 401 chars]
