Back to skill

Security audit

persistent-skill-memory

Security checks for vulnerabilities and agentic risk

Overview

This skill openly modifies an agent prompt to remember installed skills, but it lacks important validation around persistent prompt content and its generated hook script.

Install only after reviewing the exact prompt file it will modify. Prefer waiting for fixes that validate skill names, reject marker/newline/control characters, quote or redesign generated hooks, and add a clear confirmation or backup flow before prompt injection. Do not use the hook with paths or skill roots you do not fully control.

Vulnerability Patterns
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (2)

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. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/skill_memory.py:383
Finding
Generated Bash Hook Permits Command Injection Through Embedded Paths<![CDATA[ ## Vulnerability Details **File Location**: `scripts/skill_memory.py:383-397` and `scripts/skill_memory.py:401-416` **Vulnerability Type**: Shell command injection caused by unquoted path interpolation **Risk Level**: High ### Vulnerable Code The generated Bash template places substituted paths into unquoted assignment expressions: ```python HOOK_TEMPLATE = """#!/bin/bash # Wrapper: run the original installation/change command, then re-index, inject, and verify. set -euo pipefail # 1) Original command "$@" # 2) Re-index, inject, and verify SKILL_MEMORY=__TOOL_PATH__ SKILLS_ROOT=__SKILLS_ROOT__ PROMPT_FILE=__PROMPT_FILE__ python3 "$SKILL_MEMORY" index --root "$SKILLS_ROOT" python3 "$SKILL_MEMORY" inject --root "$SKILLS_ROOT" --prompt-file "$PROMPT_FILE" python3 "$SKILL_MEMORY" verify --root "$SKILLS_ROOT" --prompt-file "$PROMPT_FILE" """ ``` Caller-influenced paths are inserted by direct string replacement without shell escaping: ```python tool_path = os.path.abspath(os.path.join( os.path.dirname(os.path.abspath(__file__)), "skill_memory.py" )) content = (HOOK_TEMPLATE .replace("__TOOL_PATH__", tool_path) .replace("__SKILLS_ROOT__", os.path.abspath(args.root)) .replace("__PROMPT_FILE__", os.path.abspath(args.prompt_file))) os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True) with open(args.out, "w", encoding="utf-8", newline="") as f: f.write(content) os.chmod(args.out, os.stat(args.out).st_mode | stat.S_IXUSR | stat.S_IXGRP) ``` ### Technical Analysis Quoting variable expansions later in the script does not secure the original assignments. The generated lines are parsed by Bash before those variables are used. If an embedded path contains shell syntax such as command substitution, backticks, whitespace, semicolons, or line breaks, that syntax can change the generated script's behavior. For example, a root path containing `$(command)` causes Bash to execute `command` while evalu ...[truncated 1979 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Apply shell-safe quoting to every embedded value using `shlex.quote()`: ```python import shlex content = (HOOK_TEMPLATE .replace("__TOOL_PATH__", shlex.quote(tool_path)) .replace("__SKILLS_ROOT__", shlex.quote(os.path.abspath(args.root))) .replace("__PROMPT_FILE__", shlex.quote(os.path.abspath(args.prompt_file)))) ``` 2. Prefer avoiding generated shell source entirely. Generate a Python wrapper or a fixed Bash script that receives paths as runtime arguments. 3. Reject NUL and newline characters in all paths used to generate executable content. 4. Generate the hook atomically and use restrictive permissions such as `0700` unless group execution is explicitly required. 5. Refuse to overwrite an existing hook unless the caller supplies an explicit overwrite option. 6. Add tests for paths containing: - Spaces and tabs. - Single and double quotes. - `$()` command substitution. - Backticks. - Semicolons. - Backslashes and newlines. 7. Execute generated-hook tests in an isolated temporary directory and assert that no unintended commands or files are created. ]]>
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • System Prompt LeakageDirect Leakage, Indirect Extraction, Tool-Based Exfiltration
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (5)

os.system() or os exec-family call

High
Category
Dangerous Code Execution
Content
check(g, "模板含 index+inject+verify 三步与 set -euo pipefail",
          all(x in open(hook).read() for x in ("set -euo pipefail", "index", "inject", "verify")))
    # 成功路径:installer 成功 → 三步执行
    os.system("rm -rf %s/skills/zz-hook-check" % SK)
    w(SK + "/zz-hook-check/SKILL.md", "---\nname: zz-hook-check\ndescription: hook side effect.\n---\n\n# Z\n")
    r = subprocess.run([hook, T1 + "/inst/real_installer.sh", "arg1"],
                       capture_output=True, text=True, timeout=120, cwd=T1)
Confidence
93% confidence
Finding
The self-test uses os.system with string interpolation to run rm -rf on a constructed path. If the path component were influenced unexpectedly or contained shell metacharacters/spaces, this could trigger unintended command execution or destructive deletion; using a shell for filesystem deletion is unnecessary and risky.

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill explicitly describes capabilities to read directories, write prompt files, and generate shell wrappers, yet it declares no permissions. That mismatch can cause an agent or operator to invoke filesystem and shell-affecting behavior without clear consent boundaries, especially because the tool modifies the agent system prompt and persists state across runs.

Context-Inappropriate Capability

Medium
Confidence
90% confidence
Finding
The self-test intentionally executes installer scripts through the generated hook, expanding the test surface from deterministic offline parsing/injection into arbitrary code execution. In a security-sensitive agent-skill context, embedding a mechanism that runs external installers increases risk because a user or CI environment may execute the self-test assuming it is side-effect free.

Missing User Warnings

Low
Confidence
84% confidence
Finding
The documentation instructs prompt-file injection and replacement semantics but does not prominently warn that the target file will be modified. Because the target is an agent system prompt, silent or poorly signposted modification can persistently alter model behavior and create hard-to-audit prompt state.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
check(g, "模板含 index+inject+verify 三步与 set -euo pipefail",
          all(x in open(hook).read() for x in ("set -euo pipefail", "index", "inject", "verify")))
    # 成功路径:installer 成功 → 三步执行
    os.system("rm -rf %s/skills/zz-hook-check" % SK)
    w(SK + "/zz-hook-check/SKILL.md", "---\nname: zz-hook-check\ndescription: hook side effect.\n---\n\n# Z\n")
    r = subprocess.run([hook, T1 + "/inst/real_installer.sh", "arg1"],
                       capture_output=True, text=True, timeout=120, cwd=T1)
Confidence
94% confidence
Finding
Using rm -rf via os.system with an interpolated path creates a dangerous parameter-abuse sink: if the path is malformed, attacker-controlled, or unexpectedly expands, the command can delete unintended directories or execute injected shell syntax. Even though this is test code, the use of a recursive shell delete materially raises the blast radius if the script is run in an unsafe environment.

Static analysis

No suspicious patterns detected.