Back to skill

Security audit

Openclaw Signet

Security checks for vulnerabilities and agentic risk

Overview

This skill has a coherent local skill-verification purpose, but its file mutation and snapshot logic can delete or copy data outside the intended skill directory.

Install only if you are comfortable running a local tool that can read installed skill contents and mutate skill directories. Avoid using restore, protect, reject, snapshot, or quarantine on untrusted input until the skill-name validation, symlink handling, and overwrite/delete safeguards are fixed.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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
Findings (2)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/signet.py:480
Finding
Arbitrary Directory Deletion Through Unvalidated Skill Name## Vulnerability Details **File Location**: `scripts/signet.py:480-502` **Vulnerability Type**: Path traversal and arbitrary recursive directory deletion **Risk Level**: Critical The `restore` command constructs both the snapshot source path and restoration destination directly from the user-controlled `skill_name` argument. ```python def cmd_restore(ws, skill_name): banner("RESTORE SKILL", ws) snap_dir = snapshots_base(ws) / skill_name meta_path = snapshots_base(ws) / f"{skill_name}.json" if not snap_dir.is_dir(): print(f"No snapshot found for: {skill_name}") return 1 snap_meta = None if meta_path.exists(): try: with open(meta_path, "r", encoding="utf-8") as f: snap_meta = json.load(f) except (json.JSONDecodeError, OSError): pass snap_composite, snap_files = skill_hash(snap_dir) if snap_meta: expected = snap_meta.get("composite_hash") if expected and snap_composite != expected: print(f"SNAPSHOT CORRUPTED! Expected: {short(expected)} Got: {short(snap_composite)}") return 2 print(f" Snapshot verified: {short(snap_composite)}") else: print(" WARNING: No snapshot metadata. Restoring unverified.") skill_dir = ws / "skills" / skill_name if skill_dir.exists(): shutil.rmtree(skill_dir) shutil.copytree(str(snap_dir), str(skill_dir)) ``` ### Technical Analysis Python's `pathlib` discards the preceding path components when the right-hand operand of `/` is an absolute path. Consequently, if `skill_name` is an absolute path such as `/tmp/victim`, both of the following expressions resolve to `/tmp/victim`: - `snapshots_base(ws) / skill_name` - `ws / "skills" / skill_name` The initial `snap_dir.is_dir()` check succeeds when the attacker-selected target exists. Because snapshot metadata is optional, the comman ...[truncated 1722 chars]
Remediation
## Remediation Suggestions - Treat `skill_name` as an identifier rather than a filesystem path. Reject absolute paths, empty names, `.` and `..`, and names containing `/`, `\`, or platform-specific path separators. - Use a strict allowlist such as letters, digits, hyphens, and underscores. - Resolve both source and destination paths before performing filesystem operations and verify containment with `Path.relative_to()` or an equivalent safe check. - Require the resolved snapshot source to be a direct child of `.signet/snapshots` and the destination to be a direct child of `skills`. - Explicitly reject operations when the resolved source and destination are equal. - Require valid snapshot metadata and a matching trusted hash before any destructive restoration operation. - Copy into a newly created temporary directory under the destination base, verify the copy, and only then atomically replace the existing skill. - Avoid deleting the existing destination until all source validation and copy preparation have succeeded.

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/signet.py:69
Finding
Symlink Dereferencing Allows Files Outside a Skill to Be Read and Snapshotted## Vulnerability Details **File Location**: `scripts/signet.py:69-74`, `scripts/signet.py:455-469`, and `scripts/signet.py:612-625` **Vulnerability Type**: Improper symbolic-link handling and filesystem boundary violation **Risk Level**: High File hashing opens discovered paths normally and therefore follows symbolic links: ```python def file_hash(filepath): h = hashlib.sha256() try: with open(filepath, "rb") as f: for chunk in iter(lambda: f.read(8192), b""): h.update(chunk) return h.hexdigest() except (OSError, PermissionError): return None ``` The explicit snapshot operation copies verified skills with the default `copytree` behavior: ```python composite, files = skill_hash(skill_dir) if composite != trusted["composite_hash"]: print(f"Skill '{skill_name}' is TAMPERED. Cannot snapshot.") print(f" Expected: {short(trusted['composite_hash'])} Got: {short(composite)}") return 2 snap_dir = snapshots_base(ws) / skill_name if snap_dir.exists(): shutil.rmtree(snap_dir) shutil.copytree(str(skill_dir), str(snap_dir)) save_json(snapshots_base(ws) / f"{skill_name}.json", { "skill": skill_name, "composite_hash": composite, "files": files, "file_count": len(files), "snapshot_at": now_iso(), "signed_at": trusted.get("signed_at", "unknown"), }) ``` The `protect` operation uses the same copying behavior: ```python snap_dir = snapshots_base(ws) / name if snap_dir.exists(): shutil.rmtree(snap_dir) shutil.copytree(str(sd), str(snap_dir)) save_json(snapshots_base(ws) / f"{name}.json", { "skill": name, "composite_hash": composite, "files": files, "file_count": len(files), "snapshot_at": now_iso(), "signed_at": trusted.get("signed_at", "unknown") if trusted else "unknown", }) ``` ### Technical Analysis A file symbolic link placed inside a skill can point to a readable file o ...[truncated 2192 chars]
Remediation
## Remediation Suggestions - Reject symbolic links during hashing, signing, verification, snapshotting, and restoration. - Inspect entries with `os.lstat()` or `Path.lstat()` before opening them so validation does not dereference the link. - For every file, resolve its canonical path and require it to remain beneath the canonical skill root. - Open files using no-follow semantics where supported, such as `O_NOFOLLOW`, to reduce time-of-check/time-of-use risks. - Implement a dedicated safe-copy routine instead of relying on the default `copytree` behavior. - If symbolic links must be supported, preserve them explicitly with `symlinks=True` and separately validate that their targets are relative and remain within the skill directory. - Abort signing and snapshot creation when a link, special device, socket, FIFO, or other unsupported filesystem object is encountered. - Apply restrictive permissions to `.signet` and its snapshots to limit exposure of previously copied data. - Document the accepted filesystem object types and add tests covering links to files both inside and outside the skill root.
Vulnerability Patterns
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (7)

Lp3

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding
The skill exposes commands that invoke a local Python script over a user-supplied workspace path and explicitly describes reading installed skill files and writing a trust manifest, but it declares no explicit tool scope such as permissions or allowed-tools. That mismatch creates unnecessary ambient authority: a host may permit broader file/environment access than users expect, increasing the risk of unintended file reads or writes if the implementation is flawed or later modified.

Intent-Code Divergence

Medium
Confidence
95% confidence
Finding
The top-level documentation describes the tool as providing automatic rejection, quarantine, restoration, and broader countermeasures, suggesting default or built-in automated enforcement. In the actual implementation, these behaviors occur only through explicit CLI commands such as 'reject', 'quarantine', 'restore', or 'protect', and there is no autonomous background enforcement or any implementation corresponding to 'subvert'.

Intent-Code Divergence

Medium
Confidence
92% confidence
Finding
The message states a guaranteed runtime effect on agent behavior, but this file has no integration with an agent loader or policy engine. Its actual behavior is limited to renaming the skill directory with a quarantine prefix, so the statement overclaims enforcement beyond what the code itself does.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The restore operation unconditionally deletes the existing skill directory with shutil.rmtree before copying the snapshot back. A mistaken skill name, bad workspace selection, or maliciously crafted invocation could destroy current skill contents without an interactive safeguard, making this a destructive file-operation weakness.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
During protect, an existing quarantine directory is silently removed and replaced before the tampered skill is renamed into it. This can destroy prior forensic evidence or previously quarantined content, weakening incident response and causing data loss through a routine security action.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
When rejecting unsigned skills, the code deletes any existing destination under the quarantine area and then moves the current skill there without confirmation. This can erase previous quarantined data and cause irreversible loss from an automated cleanup path, especially if names collide or protect is run repeatedly.

Missing User Warnings

Low
Confidence
85% confidence
Finding
For markdown files, missing-warning findings apply when the description omits warnings about behaviors that could affect user data or system integrity. The README states that the skill will sign installed skills and store hashes in a manifest, but it does not explicitly disclose that running the sign command modifies files on disk.

Static analysis

No suspicious patterns detected.