Back to skill

Security audit

Dynamic Skill Manager

Security checks for vulnerabilities and agentic risk

Overview

The skill has a legitimate skill-management purpose, but its uninstall function can delete installed skills without a direct confirmation step and has a verified symlink flaw that could remove the wrong or protected skill.

Install only if you are comfortable with a local tool that can modify and delete OpenClaw skill directories. Before using uninstall, verify the target directory yourself and avoid running cleanup automatically; the symlink and confirmation issues should be fixed before trusting it for routine skill removal.

Vulnerability Patterns
  • 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
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • 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)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/skill_manager.py:189
Finding
Protected Skill Deletion Through Symlink Resolution<![CDATA[ ## Vulnerability Details **File Location**: `scripts/skill_manager.py`, lines 189-239 **Vulnerability Type**: Symlink validation after path resolution **Risk Level**: High ### Vulnerable Code ```python # Step 2: Resolve paths and verify containment skill_path = (SKILLS_DIR / skill_name).resolve() skills_dir_resolved = SKILLS_DIR.resolve() # Security check: ensure resolved path is within skills directory if not str(skill_path).startswith(str(skills_dir_resolved) + os.sep): raise ValueError( f"Security violation: skill path '{skill_path}' is outside skills directory. " "Path traversal attempt blocked." ) # Step 3: Verify the path exists and is a directory if not skill_path.exists(): return False if not skill_path.is_dir(): raise ValueError(f"Skill path '{skill_path}' is not a directory") # Step 4: Additional symlink check (prevent symlink attacks) if skill_path.is_symlink(): raise ValueError( f"Security violation: skill path '{skill_path}' is a symlink. " "Symlinks are not allowed for safety." ) registry = load_registry() # Step 5: Prevent uninstalling system skills if skill_name in SYSTEM_SKILLS and not force: raise ValueError( f"Cannot uninstall system skill '{skill_name}'. " "System skills are protected. Use --force to override (not recommended)." ) # Step 6: Archive metadata before removal if archive and skill_name in registry["skills"]: archive_file = ARCHIVE_DIR / f"{skill_name}.json" archive_data = registry["skills"][skill_name].copy() archive_data["uninstalled_at"] = datetime.utcnow().isoformat() + "Z" archive_file.write_text(json.dumps(archive_data, indent=2)) # Step 7: Remove skill directory (now safe) shutil.rmtree(skill_path) ``` ### Technical Analysis The code calls `Path.resolve()` before testing `skill_path.is_symlink()`. Resolution dereferences the original directory entry, so `is_symlink()` examines the resolved target rather tha ...[truncated 2072 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Construct the unresolved candidate path and reject symlinks before calling `resolve()`: ```python candidate = SKILLS_DIR / skill_name if candidate.is_symlink(): raise ValueError("Symlinked Skill directories are not allowed") skills_root = SKILLS_DIR.resolve(strict=True) skill_path = candidate.resolve(strict=True) ``` 2. Use path-aware containment rather than string-prefix comparison: ```python if not skill_path.is_relative_to(skills_root): raise ValueError("Resolved Skill path is outside the Skills directory") ``` 3. Verify that the resolved target corresponds to the requested Skill: ```python if skill_path.parent != skills_root or skill_path.name != skill_name: raise ValueError("Skill path does not resolve to the requested direct child") ``` 4. Apply system-Skill protection to the verified resolved target name as well as the supplied name. 5. Minimize time-of-check/time-of-use exposure by revalidating immediately before deletion. Where practical, use descriptor-relative filesystem operations that do not follow symlinks. 6. Add regression tests covering symlinks to ordinary Skills, symlinks to protected Skills, external symlink targets, nested paths, and replacement of a validated directory with a symlink before deletion. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/skill_manager.py:251
Finding
Archive Path Traversal in Archived Skill Lookup<![CDATA[ ## Vulnerability Details **File Location**: `scripts/skill_manager.py`, lines 251-255 **Vulnerability Type**: Unvalidated path traversal during local file access **Risk Level**: Medium ### Vulnerable Code ```python def get_archived_skill(skill_name: str) -> Optional[dict]: """Get archived skill metadata for re-installation.""" archive_file = ARCHIVE_DIR / f"{skill_name}.json" if archive_file.exists(): return json.loads(archive_file.read_text()) return None ``` ### Technical Analysis `get_archived_skill()` incorporates `skill_name` directly into a filesystem path without calling `validate_skill_name()` or confirming that the resolved path remains under `ARCHIVE_DIR`. Path components such as `../` are therefore interpreted by the filesystem. The forced `.json` suffix limits accessible targets to paths ending in that suffix, but it does not prevent traversal outside the archive directory. If the resulting path identifies an existing JSON file readable by the process, the function reads and returns its parsed contents. The current command-line dispatcher does not expose this function directly, which reduces immediate exploitability. However, it is a public module function intended for integration, and another component may invoke it with attacker-controlled or insufficiently trusted input. ### Attack Path 1. An application imports `get_archived_skill()` and passes a Skill name influenced by an attacker. 2. The attacker supplies a traversal value such as `../../target`. 3. The function constructs `ARCHIVE_DIR / "../../target.json"`. 4. The operating system resolves the traversal components outside `ARCHIVE_DIR`. 5. If `target.json` exists and is readable by the process, `read_text()` reads it. 6. `json.loads()` parses the file and returns its contents to the caller. ### Impact Assessment The flaw can disclose JSON files outside the intended archive directory to any caller able to control `skill_name` and observe the retur ...[truncated 480 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Apply the existing Skill-name validation before constructing the archive path: ```python def get_archived_skill(skill_name: str) -> Optional[dict]: if not validate_skill_name(skill_name): raise ValueError("Invalid skill name") ``` 2. Resolve the archive root and candidate path, then enforce path-aware containment: ```python archive_root = ARCHIVE_DIR.resolve() archive_file = (ARCHIVE_DIR / f"{skill_name}.json").resolve() if not archive_file.is_relative_to(archive_root): raise ValueError("Archive path is outside the archive directory") ``` 3. Reject archive files that are symlinks before reading them if archive entries must always be regular files. 4. Open and parse files with explicit encoding and bounded input size to avoid excessive memory consumption from unexpectedly large files. 5. Add tests for `../`, backslash traversal, absolute paths, null bytes, symlinked archive entries, nonexistent files, malformed JSON, and valid archive names. ]]>
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
Findings (5)

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill documents file-reading and file-writing behavior through its Python script usage and data locations, but it does not declare any explicit tool scope such as permissions or allowed-tools. In an agent environment, missing capability scoping can lead to overbroad file-system access being implicitly granted, increasing the chance of unintended reads, writes, or destructive lifecycle actions such as uninstall operations.

Vague Triggers

Medium
Confidence
91% confidence
Finding
The 'Use when' section includes vague conditions such as 'User wants to see what skills are installed', 'User wants to uninstall or cleanup unused skills', and generic phrases like 'skill management' and 'dynamic skill'. These triggers are broad enough to overlap with common requests and do not clearly define activation boundaries or exclusions.

Missing User Warnings

Medium
Confidence
86% confidence
Finding
This code performs irreversible deletion with shutil.rmtree() and archives metadata only optionally, but the function itself does not require explicit user confirmation before proceeding. Although there is a warning in the CLI usage path for missing arguments, a successful uninstall path only prints after deletion, so the safety-critical action lacks a direct pre-action confirmation or equivalent user disclosure at the point of execution.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
Args:
        skill_name: Name of the skill to uninstall (must be safe directory name)
        archive: Whether to archive metadata before removal
        force: Skip confirmation (use with caution)
        
    Returns:
        True if successful, False if skill not found or validation failed
Confidence
85% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Natural-Language Policy Violations

Low
Confidence
95% confidence
Finding
The file presents significant instructional content and command annotations in Chinese, while other parts are in English, with no indication that the user can choose their preferred language. This can violate a language/locale policy when a specific language is imposed without opt-in or justification.

Static analysis

No suspicious patterns detected.