T02 · Agent Memory Poisoning
Error
- Location
- scripts/skill_memory.py:201
- Finding
- Untrusted Skill Names Can Poison the Persistent Agent System Prompt<![CDATA[ ## Vulnerability Details **File Location**: `scripts/skill_memory.py:84-104`, `scripts/skill_memory.py:201-207`, and `scripts/skill_memory.py:291-307` **Vulnerability Type**: Persistent prompt injection through unsanitized Skill metadata **Risk Level**: High ### Vulnerable Code The frontmatter parser permits `name` to use multiline block-scalar syntax: ```python if key not in ("name", "description"): # Skip this key's value, including blocks and continuation lines. while i < len(fm_lines) and (not fm_lines[i].strip() or fm_lines[i].startswith((" ", "\t"))): i += 1 continue if re.match(r"^[>|][+-]?$", val): block, i = _collect_block(fm_lines, i, folded=(val[0] == ">")) fields[key] = block elif val == "": block, i = _collect_block(fm_lines, i, folded=True) fields[key] = block else: fields[key] = _unquote(val) ``` The resulting name is copied directly into the system-prompt block without validation or escaping: ```python def prompt_block(skills): """Line format: domain header [domain], followed by sorted names, one name per line.""" lines = [] for d, items in group_by_domain(skills): lines.append("[%s]" % d) lines.extend(e["name"] for e in items) return "\n".join(lines) + "\n" if lines else "" ``` The generated block is then written persistently to the selected prompt file: ```python def cmd_inject(args): if not os.path.isfile(args.prompt_file): err(2, "prompt file does not exist: %s" % os.path.abspath(args.prompt_file)) if not os.path.isdir(args.root): err(2, "skills root does not exist: %s" % os.path.abspath(args.root)) skills, _ = build_index(args.root) block = prompt_block(skills) try: status, new = inject_block(args.prompt_file, block) except OSError as e: err(2, "prompt file cannot be read or written: %s" % e) if status == "marker_error": err(2, "invalid markers") before = len(open(args.prompt_file ...[truncated 3074 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Treat all metadata read from third-party `SKILL.md` files as untrusted. 2. Enforce a canonical name grammar, such as: ```regex ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ ``` 3. Reject names containing: - Carriage returns or line feeds. - Other control characters. - Prompt marker strings. - Domain-header-shaped values. - Bidirectional or invisible Unicode control characters. 4. Do not accept YAML block scalars for `name`; limit multiline scalar handling to descriptive fields. 5. Prefer storing the index in a structured data channel that is explicitly labeled as untrusted rather than inserting raw metadata into a system prompt. 6. Construct and validate the complete candidate prompt in memory before modifying the destination file. 7. Write changes atomically through a temporary file in the same directory, then replace the destination only after validation succeeds. 8. Make `verify` compare the exact canonical block, including marker count, header order, name order, and raw block contents, rather than only comparing sets. 9. Add adversarial tests covering multiline names, marker strings, instruction-like names, fake headers, Unicode controls, and oversized metadata. ]]>
