T02 · Agent Memory Poisoning
- Location
- src/interchange.js:53
- Finding
- Persistent Indirect Prompt Injection Through Agent-Readable Interchange Markdown<![CDATA[ ## Vulnerability Details **File Location**: `src/interchange.js:53-72` and `src/interchange.js:84-97` **Vulnerability Type**: Persistent indirect prompt injection / agent memory poisoning **Risk Level**: Medium ### Vulnerable Code Profile names and descriptions are inserted directly into agent-readable Markdown: ```js function generateProfiles(dbOverride) { const db = dbOverride || getDb(); const profiles = listProfiles(db); let content = `# Voice Profiles `; profiles.forEach(p => { let desc = 'No description provided.'; try { const settings = JSON.parse(p.settings_json); desc = settings.description || desc; } catch {} content += `## ${p.name} ${desc} `; }); fs.writeFileSync(path.join(opsDir, 'profiles.md'), content); } ``` Conversation summaries are likewise inserted without escaping or trust-boundary markers: ```js function generateRecent(dbOverride) { const db = dbOverride || getDb(); const now = new Date(); now.setHours(0, 0, 0, 0); const todayStart = now.toISOString(); const recent = db.prepare(`SELECT id, summary, started FROM conversations WHERE ended IS NOT NULL ORDER BY ended DESC LIMIT 5`).all(); const todayCount = db.prepare(`SELECT COUNT(*) as count FROM conversations WHERE started >= ?`).get(todayStart).count; const durations = db.prepare(`SELECT (julianday(ended) - julianday(started)) * 86400 as duration FROM conversations WHERE ended IS NOT NULL`).all(); let totalDuration = 0; durations.forEach(d => { totalDuration += d.duration || 0; }); const totalMinutes = Math.round(totalDuration / 60); let content = `# Recent Voice Activity ## Last 5 Conversations `; recent.forEach(c => { content += `- ${c.id.substring(0, 8)}: ${c.summary || 'No summary'} (started ${c.started})\n`; }); content += ` ## Today's Conversation Count: ${todayCount} ## Total Conversation Duration: ${totalMinutes} minutes `; fs.writeFileSync(path.join(stateDir, 'recent.md'), content); } ` ...[truncated 3414 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. **Treat all database-derived content as untrusted** - Explicitly label summaries, profile names, and descriptions as user-provided data. - State in generated files that enclosed values must never be interpreted as instructions. 2. **Use structured serialization** - Prefer JSON with a fixed schema over free-form Markdown for inter-agent interchange. - Require consumers to parse specific data fields rather than ingesting the complete document as instructions. 3. **Apply context-aware encoding** - Escape Markdown headings, links, HTML, block quotes, code fences, and other structural characters before interpolation. - Normalize or reject control characters and bidirectional text controls. - Do not rely on HTML escaping alone when the consuming system is an AI agent. 4. **Separate trusted instructions from untrusted data** - Place user-controlled values inside clearly delimited data blocks. - Keep operational instructions in a separate trusted file that cannot be modified through profile or conversation inputs. - Configure consuming agents to treat interchange records exclusively as quoted evidence. 5. **Validate input** - Enforce reasonable maximum lengths for names, descriptions, and summaries. - Restrict profile names to a conservative character set. - Validate settings against an explicit JSON schema rather than accepting arbitrary objects. 6. **Harden downstream consumers** - Instruct consuming agents not to follow directives found in summaries, transcripts, profile metadata, or other user-controlled fields. - Require confirmation or policy checks before executing tools based on interchange content. 7. **Add security regression tests** - Test summaries and descriptions containing headings, links, HTML, code fences, and phrases such as `Ignore previous instructions`. - Verify that generated output preserves such values only as inert, clearly identified data. ]]>
