T02 · Agent Memory Poisoning
Error
- Location
- scripts/apply_approved_patches.py:30
- Finding
- Untrusted Patch Approval Allows Persistent Agent Memory and Instruction Poisoning<![CDATA[ ## Vulnerability Details **File Location**: `scripts/apply_approved_patches.py:30-80` **Vulnerability Type**: Approval integrity failure permitting persistent instruction injection **Risk Level**: High ### Vulnerable Code ```python def validate_patch(patch: dict): target = patch.get("target_file") if target not in TARGETS: return f"target_file not allowed: {target}" if not patch.get("approved"): return "not approved" if not (patch.get("suggested_entry") or (patch.get("old_text") is not None and patch.get("new_text") is not None)): return "no applicable patch content" return None def apply_patch(base_dir: Path, patch: dict, dry_run: bool = False): error = validate_patch(patch) if error: return {"id": patch.get("id"), "status": "skipped" if error == "not approved" else "error", "reason": error} path = base_dir / patch["target_file"] path.parent.mkdir(parents=True, exist_ok=True) if not path.exists() and not dry_run: path.write_text("", encoding="utf-8") text = path.read_text(encoding="utf-8") if path.exists() else "" old_text = patch.get("old_text") new_text = patch.get("new_text") suggested_entry = patch.get("suggested_entry") anchor = patch.get("anchor") insert_mode = patch.get("insert_mode", "append") if suggested_entry and suggested_entry in text: return {"id": patch.get("id"), "status": "skipped", "reason": "entry already present", "target": str(path)} if old_text is not None and new_text is not None: if old_text not in text: return {"id": patch.get("id"), "status": "error", "reason": "old_text not found", "target": str(path)} updated = text.replace(old_text, new_text, 1) if not dry_run: path.write_text(updated, encoding="utf-8") return {"id": patch.get("id"), "status": "applied", "mode": "replace", "target": str(path), "dry_run": dry_run} if suggested_entry and ...[truncated 3787 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. **Separate approval records from patch content** - Store approvals in a distinct, access-controlled file or trusted approval service. - Do not treat a Boolean embedded in the candidate document as authoritative. 2. **Cryptographically bind approval to exact content** - Canonicalize each patch and calculate a strong hash over its identifier, target, mode, anchor, old text, and new content. - Record the hash when the reviewer approves the patch. - Recalculate and compare the hash immediately before application. - Reject any patch changed after approval. 3. **Require final-diff confirmation** - Render the exact destination path and final diff immediately before writing. - Require explicit confirmation for each patch, especially changes to `SOUL.md`, `AGENTS.md`, `TOOLS.md`, and `MEMORY.md`. - For non-interactive automation, require a separately generated signed approval artifact. 4. **Validate patch structure and semantics** - Enforce a strict JSON schema, including exact Boolean types, permitted insertion modes, size limits, and required fields. - Flag instruction-like content involving safety overrides, credential access, destructive commands, external payloads, or privilege changes for elevated review. - Reject ambiguous or malformed replacement operations. 5. **Harden filesystem writes** - Resolve and verify the base directory and destination paths. - Reject symbolic-link destinations or use no-follow file operations to prevent unexpected redirection. - Write through a temporary file in the same directory and atomically replace the destination. - Create restricted-permission backups and provide a rollback mechanism. 6. **Improve auditability** - Record the reviewer identity, timestamp, canonical patch hash, destination file hash before and after modification, and exact applied diff. - Keep append-only or tamper-evident audit logs. ]]>
