T02 · Agent Memory Poisoning
- Location
index.js:54- Finding
Persistent Agent Memory Poisoning Through Unsanitized DingTalk Metadata
- Content
View full analysis
Vulnerability Details
File Location:
index.js:54-80, 92-141
Vulnerability Type: Persistent injection of attacker-controlled content into agent memory
Risk Level: HighVulnerable 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
groupNameandconversationIdfrom 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
View remediation
Remediation Suggestions
-
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.
-
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.
-
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.
- Escape
-
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.
-
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.
- Validate and sanitize existing JSON records before generating
-
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]-
