Back to skill

Security audit

Clawscan Vigil

Security checks for vulnerabilities and agentic risk

Overview

This is a real local skill scanner, but its default dynamic analysis runs scanned Python code in-process and its coverage can understate risk for non-Python or instruction-only skills.

Install only if you are comfortable reviewing its behavior and preferably run scans with dynamic analysis disabled or inside a disposable sandbox. Do not rely on a LOW result for non-Python, instruction-only, JavaScript, shell, or mixed-language skills, and be aware it records local quota/license files under your home directory.

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 (4)

T09 · Insecure Skill Coding Practices

Error
Location
core/dynamic_tracer.py:34
Finding
Dynamic analysis executes untrusted code without enforcing resource limits or a timeout<![CDATA[ ## Vulnerability Details **File Location**: `core/dynamic_tracer.py:34-35, 48-55, 73-83, 115-129` **Vulnerability Type**: Uncontrolled execution of untrusted code **Risk Level**: High ### Vulnerable Code ```python def __init__(self, timeout: float = 5.0): self.timeout = timeout self.trace = ExecutionTrace() ``` ```python # Check if code is safe to attempt execution if not self._is_safe_to_execute(code): findings.append(Finding( level=RiskLevel.MEDIUM, category="dynamic_analysis_skipped", description="Code contains constructs unsafe for dynamic analysis", file=str(file_path), line=0, confidence=0.7, )) return findings ``` ```python # Execute in sandbox exec(compiled, restricted_globals) self.trace.completed = True ``` ```python def _is_safe_to_execute(self, code: str) -> bool: """Quick check if code looks safe to execute""" dangerous_patterns = [ "while True:", # Infinite loops "__import__", "eval(", "exec(", ] code_lower = code.lower() for pattern in dangerous_patterns: if pattern in code_lower: return False return True ``` ### Technical Analysis The scanner performs in-process execution of Python files supplied by an untrusted Skill. Although the constructor accepts a five-second timeout, `self.timeout` is never used to interrupt or terminate execution. The safety check is a substring denylist rather than a resource-control boundary. It only detects a few exact textual forms. Equivalent resource-exhaustion constructs such as `while 1:`, computationally expensive loops, deep recursion, or memory-intensive expressions are not rejected. RestrictedPython constrains access to selected Python operations, but it does not independently enforce wall-clock, CPU, or memory limits. Because execution occurs synchronously in the scanner process, a resource-exhaustion payload can hang or terminate the security tool ...[truncated 1197 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Do not execute untrusted Skill code inside the main scanner process. - Move each dynamic-analysis job into a disposable child process, container, or sandboxed virtual machine. - Enforce a real wall-clock timeout and forcibly terminate the entire child process tree when it expires. - Apply operating-system CPU, address-space, process-count, and open-file limits. - Disable networking and provide a read-only or disposable filesystem. - Run the worker under a dedicated, unprivileged account with no access to user credentials or sensitive environment variables. - Default to static analysis unless the user explicitly enables dynamic execution after receiving a clear warning. - Treat timeout, memory-limit, or sandbox failures as security findings rather than silently continuing. - Add regression tests covering alternate infinite loops, recursion, large allocations, process termination, and timeout cleanup. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
core/scanner.py:38
Finding
Unsupported and instruction-bearing Skill files are skipped and may be reported as low risk<![CDATA[ ## Vulnerability Details **File Location**: `core/scanner.py:38-52, 70` **Vulnerability Type**: Fail-open security scanning and incomplete content coverage **Risk Level**: High ### Vulnerable Code ```python # Collect all Python files to scan if skill_path.is_file() and skill_path.suffix == ".py": py_files = [skill_path] elif skill_path.is_dir(): py_files = list(skill_path.rglob("*.py")) # Also check for SKILL.md for metadata skill_md = skill_path / "SKILL.md" if skill_md.exists(): py_files.append(skill_md) # Will be ignored by analyzer but counted else: py_files = [] # Scan each Python file for py_file in py_files: if py_file.suffix != ".py": continue ``` ```python # Calculate overall risk overall_risk = self.risk_engine.calculate(all_findings, skill_name) ``` The resulting empty-finding behavior is defined in `core/risk_engine.py:32-33`: ```python if not findings: return RiskLevel.LOW ``` ### Technical Analysis The scanner claims to assess OpenClaw Skills, but it only analyzes files ending in `.py`. It explicitly appends `SKILL.md` and then skips it because of the suffix check. Shell scripts, JavaScript, TypeScript, configuration files, installation hooks, and other potentially executable or instruction-bearing content are not assessed. This is particularly significant for an Agent Skill because `SKILL.md` can define behavior and instructions even when the package contains no Python code. A malicious instruction-only Skill or a Skill implemented in an unsupported language can produce no findings. The risk engine then converts the absence of findings into `LOW`, conflating “nothing supported was analyzed” with “the Skill is safe.” ### Attack Path 1. An attacker places harmful instructions, external download commands, or non-Python executable behavior in `SKILL.md`, a shell script, JavaScript, or another unsupported file. 2. The victim scans the Skill before installation. 3. The scanner collect ...[truncated 789 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Parse and assess `SKILL.md` as security-relevant input rather than appending and skipping it. - Detect instruction hijacking, safety-constraint overrides, forced output behavior, sensitive-file requests, installation commands, external downloads, and encoded payload instructions. - Add analyzers for all supported executable formats, including shell, JavaScript, TypeScript, PowerShell, and relevant configuration or manifest files. - Inventory every file in the Skill and explicitly report unsupported executable or instruction-bearing formats. - Return `UNKNOWN`, `INCOMPLETE`, or a blocking scan error when meaningful content cannot be analyzed; never convert absent coverage into `LOW`. - Include coverage statistics in the result: files discovered, files analyzed, files skipped, supported languages, and skip reasons. - Consider package installation metadata and entry points when identifying executable behavior. - Add test fixtures for instruction-only Skills, shell-only Skills, mixed-language packages, nested `SKILL.md` files, and packages containing no Python. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
core/dynamic_tracer.py:34
Finding
Dynamic execution traces persist across files and batch-scanned Skills<![CDATA[ ## Vulnerability Details **File Location**: `core/dynamic_tracer.py:34-35`; related reuse at `core/scanner.py:18-21, 48-65` and `core/batch_scanner.py:57-68` **Vulnerability Type**: Cross-target analysis-state contamination **Risk Level**: Medium ### Vulnerable Code ```python def __init__(self, timeout: float = 5.0): self.timeout = timeout self.trace = ExecutionTrace() ``` The same tracer is retained by one scanner: ```python def __init__(self, enable_dynamic: bool = True): self.static_analyzer = StaticAnalyzer() self.dynamic_tracer = DynamicTracer() if enable_dynamic else None self.risk_engine = RiskEngine() self.enable_dynamic = enable_dynamic ``` It is then reused for every file: ```python # Dynamic analysis (if enabled) if self.enable_dynamic and self.dynamic_tracer: try: code = py_file.read_text(encoding="utf-8", errors="ignore") dynamic_findings = self.dynamic_tracer.analyze(code, py_file) all_findings.extend(dynamic_findings) except Exception: # Dynamic analysis failure shouldn't stop the scan pass ``` Batch mode also reuses one scanner across Skills: ```python scanner = Scanner() results = [] with Progress( SpinnerColumn(), TextColumn("[progress.description]{task.description}"), console=console, ) as progress: task = progress.add_task("Scanning...", total=len(skills)) for skill_path in skills: progress.update(task, description=f"Scanning {skill_path.name}...") try: result = scanner.scan_skill(skill_path, skill_path.name) results.append(result) ``` ### Technical Analysis `ExecutionTrace` is initialized only when `DynamicTracer` is constructed. `DynamicTracer.analyze()` does not reset it before analyzing a new file. Consequently, imports, function calls, file accesses, URLs, errors, and completion state accumulate across analyses. `Scanner` reuses the same tracer across every Python fil ...[truncated 1306 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Set `self.trace = ExecutionTrace()` at the beginning of every `analyze()` invocation. - Prefer making the trace a local variable and passing it explicitly to helper functions. - Instantiate separate analyzer state for each file and separate scanner state for each Skill. - Ensure batch scanning cannot share mutable findings, traces, mocks, errors, or completion flags between targets. - Add tests that scan a malicious file followed by a benign file and verify that no findings carry over. - Add equivalent isolation tests across separate Skills in batch mode. - Include the originating file and invocation identity in trace events to preserve reliable attribution. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
core/license_manager.py:75
Finding
Any locally generated key with the expected format unlocks premium authorization<![CDATA[ ## Vulnerability Details **File Location**: `core/license_manager.py:75-99, 101-132` **Vulnerability Type**: Authentication and authorization bypass **Risk Level**: Medium ### Vulnerable Code ```python def has_valid_license(self) -> bool: """Check if user has valid paid license""" if not self.license_file.exists(): return False try: license_data = json.loads(self.license_file.read_text()) # Check expiration expires = license_data.get("expires") if expires: expiry_date = datetime.fromisoformat(expires) if datetime.now() > expiry_date: return False # Verify license key format (basic check) key = license_data.get("key", "") if not self._verify_key_format(key): return False return True except: return False ``` ```python def _verify_key_format(self, key: str) -> bool: """Basic license key format verification""" # Format: CLAW-XXXX-XXXX-XXXX parts = key.split("-") if len(parts) != 4 or parts[0] != "CLAW": return False # Check each part is 4 alphanumeric chars for part in parts[1:]: if len(part) != 4 or not part.isalnum(): return False return True ``` ```python def activate_license(self, key: str) -> bool: """ Activate a license key. Returns True if successful. """ if not self._verify_key_format(key): return False # In real implementation, would verify with server # For now, accept any properly formatted key license_data = { "key": key, "activated": datetime.now().isoformat(), "expires": (datetime.now() + timedelta(days=365)).isoformat(), "type": "personal", "verified": False # Would be True after server verification } self.license_file.write_text(json.dumps(license_data, indent=2)) return True ``` ### ...[truncated 1604 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Use server-issued licenses signed with an asymmetric private key and verify them locally using an embedded public key. - Bind the signed claims to the product, entitlement tier, issue time, and expiration time. - Reject licenses whose signature is absent or invalid. - If online activation is used, require authenticated TLS communication and store a server-issued, verifiable activation token. - Do not trust unsigned `key`, `expires`, `type`, or `verified` values from a user-writable JSON file. - Require `verified` to be cryptographically established rather than treating it as an ordinary local Boolean. - Protect the stored license file with user-only permissions as defense in depth, while recognizing that permissions do not prevent the owning user from altering it. - Add negative tests for random formatted keys, edited expiry dates, altered tiers, invalid signatures, and replayed licenses. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
Findings (26)

Tool Parameter Abuse

High
Category
Tool Misuse
Content
```bash
# 清理缓存
rm -rf core/__pycache__ tests/__pycache__
rm -rf build/ dist/ *.egg-info

# 创建发布包
Confidence
85% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Tool Parameter Abuse

High
Category
Tool Misuse
Content
```bash
# 清理缓存
rm -rf core/__pycache__ tests/__pycache__
rm -rf build/ dist/ *.egg-info

# 创建发布包
mkdir -p clawscan-skill
Confidence
85% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Tool Parameter Abuse

High
Category
Tool Misuse
Content
# 创建发布包
mkdir -p clawscan-skill
cp -r core cli.py pyproject.toml README.md skill/ clawscan-skill/
rm -rf clawscan-skill/core/__pycache__

# 打包
tar -czf clawscan-v0.2.0.tar.gz clawscan-skill/
Confidence
85% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
声明描述聚焦于“安装前扫描 OpenClaw Skill 安全风险,静态+动态双重检测,识别恶意代码”。而这段代码虽然确实调用 Scanner 执行扫描,并支持关闭动态分析,整体方向与安全扫描相关,但其主要可见实现内容显著包含未声明的商业化许可证管理与使用配额控制能力:检查有效许可证、记录免费扫描次数、激活许可证、显示订阅状态、限制批量扫描和依赖分析为 Premium 功能,并输出升级营销信息。这些都属于实质性的额外能力,而非纯粹的扫描实现细节。此外,声明强调“安装前扫描”,但代码中的 check 命令会定位并扫描本地已安装技能,行为范围超出仅“安装前”。因此描述未能准确覆盖代码实际行为,存在明显描述-行为不匹配。

Tp4

High
Category
MCP Tool Poisoning
Confidence
94% confidence
Finding
声明描述该技能会在安装前对 OpenClaw Skill 做“静态+动态双重检测”并“识别恶意代码”。但提供的代码块仅展示了一个高级分析模块:读取依赖文件、匹配已知高风险/可疑包名、汇总已有 findings 并生成扫描报告。这属于静态依赖风险分析与报告生成的支持功能,没有看到动态执行、沙箱监控、行为观测等动态检测能力,也没有直接实现对代码内容的全面恶意行为识别。因此,实际行为只覆盖了声明中的部分静态扫描/风险评估,未达到所宣称的主要能力范围,构成描述与实现不符。

Tp4

High
Category
MCP Tool Poisoning
Confidence
87% confidence
Finding
代码总体上仍属于“扫描 Skill 安全风险”的范畴,因此与声明有一定相关性;但声明强调的是安装前安全扫描、静态+动态双重检测、识别恶意代码,而该代码块实际是一个批量扫描器的外围控制逻辑:查找 Skill 目录、逐个调用扫描器、统计高/中/低风险、展示表格并导出 JSON。这里没有看到动态执行、沙箱、行为监控等动态检测实现,也没有直接展示恶意代码识别机制。另有批量扫描和导出功能属于未声明能力。由于核心方向相近但描述对能力表述更强、更具体,判断为存在描述与行为不完全一致的情况。

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
The declared description says the skill scans OpenClaw skills for security risks via static and dynamic detection of malicious code. However, this code chunk contains no scanning, code analysis, threat detection, or install-time security checks. Its primary function is license management and enforcement of a free monthly scan quota. It reads and writes license.json and usage.json in the user's home directory, checks expiration dates, validates a basic key format, and records usage. These are materially different from the declared purpose, so this is a clear description-behavior mismatch.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
存在实质性描述不符。声明强调“安装前”安全扫描以及“静态+动态双重检测、识别恶意代码”,但该代码片段的可见行为只是调用外部扫描器 `clawscan` 处理本地目录,或在若干本地安装目录中查找已安装 skill 后进行扫描。`check_clawhub_skill` 甚至明确要求 skill 必须先安装,和“安装前扫描”相反。另一个关键点是,本代码并未自身实现静态/动态分析或恶意代码识别逻辑;虽然底层 `clawscan` 可能具备这些能力,但从提供代码来看,这些能力并非代码块本身可验证的实际行为。因此其主要用途相关,但核心能力表述被夸大且与实际执行条件不一致,应判定为 mismatch。

exec() call detected

High
Category
Dangerous Code Execution
Content
return findings
            
            # Execute in sandbox
            exec(compiled, restricted_globals)
            self.trace.completed = True
            
        except Exception as e:
Confidence
92% confidence
Finding
The code dynamically executes untrusted skill code via exec() after only superficial string checks and a RestrictedPython wrapper. This is dangerous because sandbox implementations are historically bypassable, the allowlist here exposes powerful hooks like getattr/setattr, and a failure in the sandbox would let attacker-controlled code run during scanning.

Credential Access

High
Category
Privilege Escalation
Content
"filesystem_read_sensitive": {
        "level": RiskLevel.HIGH,
        "patterns": [
            r"open\s*\([^)]*\.env",
            r"open\s*\([^)]*config",
            r"open\s*\([^)]*secret",
            r"open\s*\([^)]*key",
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Natural-Language Policy Violations

Medium
Confidence
93% confidence
Finding
This markdown file contains user-facing operational guidance only in Chinese, including installation, publishing, pricing, and roadmap details. Under the policy rule for language/locale, forcing a specific language without user opt-in or justification is a natural-language policy concern.

Natural-Language Policy Violations

Medium
Confidence
94% confidence
Finding
The entire skill description and usage guidance are presented in Chinese, including headings, explanations, and operational details, with no indication that users may choose another language. Under the policy, forcing a specific language without user opt-in is a natural-language policy violation unless clearly justified as region-specific.

Lp3

Medium
Category
MCP Least Privilege
Confidence
70% confidence
Finding
Without declared permissions the skill's intent is opaque and cannot be validated.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The manifest description and the entire user-facing README content are written in Chinese, with no indication that other languages are supported or that the skill is region-specific. Under the policy, forcing a specific language without user opt-in is a natural-language locale violation.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The scanner executes submitted code automatically without any explicit user-facing disclosure or consent, which is risky because users may assume analysis is purely static while the tool actually runs attacker-supplied code. In this skill context, that increases danger because the component is meant to inspect potentially malicious skills, so hidden execution expands the attack surface exactly where adversarial input is expected.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
This markdown file uses Chinese for the description, headings, instructions, and support information, but it does not indicate that the skill is region-specific or provide an opt-in language/locale choice. That can violate a language/locale policy when users are not given a choice of language.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
if verbose:
        cmd.append("--verbose")
    
    result = subprocess.run(cmd, capture_output=True, text=True)
    
    if result.returncode <= 2:  # 0, 1, 2 are valid scan results
        return json.loads(result.stdout)
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Missing User Warnings

Low
Confidence
79% confidence
Finding
The `activate` command takes a license key as a positional CLI argument, which is a sensitive credential-like value. While the command's purpose is license activation, there is no visible warning that passing the key on the command line may expose it through shell history or process listings.

Missing User Warnings

Low
Confidence
84% confidence
Finding
The skill stores usage information in a persistent file under the user's home directory, but there is no visible user disclosure such as a prompt, log message, or explanatory comment indicating that scan counts will be recorded. This is a file write affecting user data and falls under the missing-warning criteria for code files.

Missing User Warnings

Low
Confidence
88% confidence
Finding
This code writes a license file to the user's home directory, which affects local user data, but the activation path provides no confirmation prompt, print/log notice, or comment warning beyond the internal docstring. For a code file, file writes that modify persistent user data should have some visible disclosure unless the warning is documented elsewhere.

Unverifiable Dependency: click has 1 known advisory(ies) (CVE-2026-7246 (Pallets Click, versions 8.3.2 and below, contain a command injection vulnerabili)), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
93% confidence
Finding
The dependency specification for click uses a lower-bound only (>=8.0) rather than a pinned or tightly constrained version, so the security posture of the installed package cannot be verified from this manifest alone. Because the scanner itself is a security-sensitive tool, relying on floating dependency resolution increases supply-chain risk and may allow installation of versions affected by known advisories or unexpected breaking behavior.

Unverifiable Dependency: pyyaml has 8 known advisory(ies) (CVE-2019-20477 (Deserialization of Untrusted Data in PyYAML); CVE-2020-1747 (Improper Input Validation in PyYAML); CVE-2020-14343 (Improper Input Validation in PyYAML) +5 more), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
96% confidence
Finding
PyYAML has a long history of unsafe deserialization issues, and this manifest does not pin the resolved version, leaving uncertainty about whether vulnerable releases could be installed. In a tool that scans untrusted skill content, YAML parsing is especially security-relevant because attacker-controlled input may be processed during analysis workflows.

Unverifiable Dependency: requests has 16 known advisory(ies) (CVE-2014-1830 (Exposure of Sensitive Information to an Unauthorized Actor in Requests); CVE-2024-47081 (Requests vulnerable to .netrc credentials leak via malicious URLs); CVE-2024-35195 (Requests `Session` object does not verify requests after making first request wi) +13 more), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
95% confidence
Finding
Requests is declared with a broad lower-bound only, making it impossible to verify whether installations avoid versions with known advisories. Since a security scanner may fetch remote content, update feeds, or repositories, a vulnerable HTTP client can increase exposure to credential leakage, TLS-related issues, or unsafe URL handling depending on runtime usage.

Unverifiable Dependency: RestrictedPython has 10 known advisory(ies) (CVE-2024-47532 (RestrictedPython information leakage via `AttributeError.obj` and the `string` m); CVE-2026-55830 (RestrictedPython guard hooks can be shadowed via positional-only arguments); CVE-2025-22153 (try/except* clauses could allow bypass RestrictedPython via type confusion bug i) +7 more), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
98% confidence
Finding
RestrictedPython is security-critical sandboxing technology, and leaving its version unconstrained is risky because multiple advisories describe sandbox bypasses and information leaks. In the context of a scanner that may dynamically inspect or execute untrusted skill logic, a vulnerable sandbox dependency materially increases the chance of code execution or containment failure.

Unverifiable Dependency: pytest has 2 known advisory(ies) (CVE-2025-71176 (pytest has vulnerable tmpdir handling); CVE-2025-71176 (pytest has vulnerable tmpdir handling)), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
40% confidence
Finding
Dependency has known vulnerabilities (CVEs). Using packages with unpatched security flaws exposes the environment to known exploits.

Static analysis

Detected: suspicious.dynamic_code_execution

Dynamic code execution detected.

Critical
Code
suspicious.dynamic_code_execution
Location
core/dynamic_tracer.py:122