Back to skill

Security audit

Evez Skill Vetter

Security checks for vulnerabilities and agentic risk

Overview

This is a local skill-security scanner, but it handles untrusted folders too loosely and can read outside the chosen folder through links or consume excessive resources.

Only run this vetter in a constrained directory or sandbox, and avoid using it directly on fully untrusted skill archives until symlink rejection, file-size limits, and terminal-safe path escaping are added. It does not show evidence of malicious behavior, but its current boundaries are too weak for a security tool.

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)

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/vet.py:98
Finding
Symbolic-Link Traversal Allows Reading Files Outside the Audited Skill## Vulnerability Details **File Location**: `scripts/vet.py:98-106` **Vulnerability Type**: Symbolic-link traversal and unauthorized local file access **Risk Level**: Medium ### Vulnerable Code ```python def _scan_files(self): for filepath in self.skill_path.rglob("*"): if filepath.is_dir(): continue if filepath.suffix in ('.pyc', '.pyo', '.so', '.dll', '.exe'): self.findings.append(Finding("danger", "binary", f"Binary file found: {filepath.name}", str(filepath), 0, 25)) continue try: content = filepath.read_text(encoding="utf-8", errors="ignore") except Exception: continue ``` ### Technical Analysis The scanner treats the audited Skill directory as untrusted input but does not reject symbolic links or verify that each resolved file remains inside `self.skill_path`. `Path.read_text()` follows a symbolic link when it points to a readable file. An attacker can therefore package a symbolic link whose apparent location is inside the Skill but whose target is an arbitrary file available to the user running the audit. The target's contents are then loaded and inspected by the scanner. This behavior violates least privilege because auditing a Skill only requires access to files physically contained within that Skill. The scanner should not follow references into unrelated portions of the local filesystem. ### Attack Path 1. An attacker creates a Skill directory containing a symbolic link, such as `linked-secret`, that points to a likely sensitive local path. 2. The victim obtains the untrusted Skill and runs `python3 scripts/vet.py --skill /path/to/skill`. 3. `rglob("*")` discovers the symbolic link as an entry in the audited directory. 4. The scanner does not reject the link or validate its resolved destination. 5. `filepath.read_text()` follows the link and reads the external file. 6. The external contents are ...[truncated 647 chars]
Remediation
## Remediation Suggestions - Reject symbolic links before reading any entry: ```python if filepath.is_symlink(): self.findings.append(Finding( "warn", "symlink", "Symbolic links are not scanned", str(filepath.relative_to(self.skill_path)), 0, 0 )) continue ``` - Resolve the audit root once and confirm that every candidate's resolved path remains beneath it: ```python root = self.skill_path.resolve() try: resolved = filepath.resolve(strict=True) resolved.relative_to(root) except (FileNotFoundError, RuntimeError, ValueError): continue ``` - Perform containment validation immediately before opening the file to reduce time-of-check/time-of-use exposure. - Open files using directory-relative, no-follow operating-system primitives where supported, such as `os.open()` with `O_NOFOLLOW`. - Apply the same policy to file-size calculation and any future archive or dependency inspection logic.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/vet.py:98
Finding
Unbounded File Reads Allow Memory-Exhaustion Denial of Service## Vulnerability Details **File Location**: `scripts/vet.py:98-106` **Vulnerability Type**: Uncontrolled resource consumption **Risk Level**: Medium ### Vulnerable Code ```python def _scan_files(self): for filepath in self.skill_path.rglob("*"): if filepath.is_dir(): continue if filepath.suffix in ('.pyc', '.pyo', '.so', '.dll', '.exe'): self.findings.append(Finding("danger", "binary", f"Binary file found: {filepath.name}", str(filepath), 0, 25)) continue try: content = filepath.read_text(encoding="utf-8", errors="ignore") except Exception: continue ``` The aggregate size check is performed only after all files have already been read: ```python def vet(self) -> dict: self._check_structure() self._scan_files() self._check_size() self.total_risk = min(100, sum(f.score_impact for f in self.findings)) return self.report() ``` ```python def _check_size(self): total = sum(f.stat().st_size for f in self.skill_path.rglob("*") if f.is_file()) if total > 5_000_000: self.findings.append(Finding("warn", "size", f"Skill is {total//1024}KB — suspiciously large", "", 0, 10)) ``` ### Technical Analysis Every non-excluded file is loaded completely into a Python string with `read_text()`. There is no per-file limit, cumulative byte limit, file-count limit, timeout, or streaming strategy. Although `_check_size()` recognizes Skills larger than 5 MB, it runs after `_scan_files()`. Consequently, the threshold only creates a finding and does not protect the scanning process. A large regular or sparse file can force substantial memory allocation before the scanner reports that the Skill is oversized. ### Attack Path 1. An attacker places a very large text file or a large sparse file in a Skill package. 2. The victim invokes the vetter on that untrusted directory. 3. `_s ...[truncated 846 chars]
Remediation
## Remediation Suggestions - Enforce cumulative, per-file, and file-count limits before scanning content. - Move size validation before `_scan_files()` and abort or skip scanning when limits are exceeded. - Use `stat()` without following symbolic links and account for errors and special files. - Read regular files incrementally in bounded chunks rather than loading them completely: ```python MAX_FILE_SIZE = 1_000_000 MAX_TOTAL_SIZE = 5_000_000 size = filepath.stat().st_size if size > MAX_FILE_SIZE: continue with filepath.open("r", encoding="utf-8", errors="ignore") as handle: for line_num, line in enumerate(handle, 1): # Apply patterns to one bounded line or chunk at a time. ... ``` - Reject non-regular files, including devices, FIFOs, and sockets. - Consider process-level memory and execution-time limits when scanning fully untrusted packages. - Ensure exceptionally long individual lines are also bounded, because line-by-line iteration alone does not constrain a single enormous line.

T09 · Insecure Skill Coding Practices

Note
Location
scripts/vet.py:150
Finding
Attacker-Controlled Filenames Are Printed Without Terminal-Safe Escaping## Vulnerability Details **File Location**: `scripts/vet.py:150-155` **Vulnerability Type**: Terminal control-sequence injection **Risk Level**: Low ### Vulnerable Code ```python if skill: vetter = SkillVetter(Path(skill)) report = vetter.vet() level = report["risk_level"].upper() icons = {"safe": "✅", "caution": "⚠️", "risky": "🚨", "dangerous": "❌"} click.echo(f"\n{icons.get(level, '?')} RISK: {report['risk_score']}/100 ({level})") click.echo(f"Findings: {report['findings_count']}\n") for f in report["findings"]: icon = {"info": "ℹ️", "warn": "⚠️", "danger": "🚨"}.get(f["severity"], "?") click.echo(f" {icon} {f['category']}: {f['message']} ({f['file']}:{f['line']})") ``` The displayed path originates from an untrusted filesystem entry: ```python rel_path = str(filepath.relative_to(self.skill_path)) ``` ### Technical Analysis Filesystem names under the audited directory are attacker-controlled. The relative path is inserted directly into terminal output without escaping control characters. A filename can contain carriage returns, newlines, tabs, backspaces, or terminal escape sequences. When such a filename produces a finding, `click.echo()` writes those characters to the terminal. Depending on terminal behavior, this can overwrite visible lines, alter colors, clear output, forge additional findings, manipulate terminal hyperlinks, or otherwise obscure the audit result. The vulnerability affects output integrity rather than the scanner's pattern-matching logic. It is especially relevant because users rely on this tool's terminal report to make security decisions. ### Attack Path 1. An attacker creates a file whose name contains terminal control characters or an escape sequence. 2. The attacker places text matching one of the scanner's detection patterns inside that file. 3. The victim runs the scanner from an interactive terminal. 4. The filename is ...[truncated 693 chars]
Remediation
## Remediation Suggestions - Escape every attacker-controlled path before displaying it. - Use a representation that visibly encodes control characters, such as `ascii(path)` or an equivalent dedicated helper. - Preserve printable Unicode while encoding C0, DEL, C1, carriage-return, newline, and escape characters. - Apply the same escaping to all future output formats that may be interpreted by a terminal. - Prefer structured JSON output for automation and encode filenames according to the JSON specification. - Add tests using filenames containing newline, carriage return, backspace, and ANSI escape characters to verify that each is displayed as inert text.
Vulnerability Patterns
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • 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)

Credential Access

High
Category
Privilege Escalation
Content
(r'XMLHttpRequest', "network_access", "XHR makes network requests", 6),
    (r'localStorage\.', "browser_storage", "Access to browser localStorage", 3),
    (r'document\.cookie', "cookie_access", "Access to cookies", 8),
    (r'process\.env', "env_access", "Reads environment variables", 4),
    (r'child_process', "command_exec", "Node.js child_process can run commands", 18),
    (r'require\s*\([\'"]child_process', "command_exec", "Requires child_process module", 18),
    (r'\.exec\s*\(', "command_exec", ".exec() can run commands", 15),
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Lp3

Medium
Category
MCP Least Privilege
Confidence
84% confidence
Finding
The skill metadata does not declare a permission/tool scope, yet the skill description and quick-start instructions indicate capabilities involving file access, network access, and shell execution. This creates a transparency and policy-enforcement gap: reviewers or installers cannot reliably assess or constrain what the skill may do before use, increasing the risk of over-privileged execution or unnoticed dangerous behavior.

Unrestricted Tool Access

Medium
Category
Excessive Agency
Content
DANGEROUS_PATTERNS = [
    (r'\beval\s*\(', "code_injection", "eval() can execute arbitrary code", 25),
    (r'\bexec\s*\(', "code_injection", "exec() can execute arbitrary code", 20),
    (r'\bsubprocess\.\w+\(', "command_exec", "subprocess can run system commands", 15),
    (r'\bos\.system\s*\(', "command_exec", "os.system() runs shell commands", 20),
Confidence
80% confidence
Finding
Skill grants unrestricted tool access without appropriate constraints. An agent with unfettered tool access can perform arbitrary actions including file modification, network requests, and code execution.

Unrestricted Tool Access

Medium
Category
Excessive Agency
Content
DANGEROUS_PATTERNS = [
    (r'\beval\s*\(', "code_injection", "eval() can execute arbitrary code", 25),
    (r'\bexec\s*\(', "code_injection", "exec() can execute arbitrary code", 20),
    (r'\bsubprocess\.\w+\(', "command_exec", "subprocess can run system commands", 15),
    (r'\bos\.system\s*\(', "command_exec", "os.system() runs shell commands", 20),
Confidence
80% confidence
Finding
Skill grants unrestricted tool access without appropriate constraints. An agent with unfettered tool access can perform arbitrary actions including file modification, network requests, and code execution.

Missing User Warnings

Medium
Confidence
85% confidence
Finding
This code traverses the entire provided directory tree and reads each file's contents, which can expose user data contained in the skill directory. While the behavior is central to vetting, the file itself provides no docstring, comment, or prompt disclosing that all files under the path will be opened and inspected.

Static analysis

Detected: suspicious.dangerous_exec, suspicious.dynamic_code_execution

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/vet.py:23

Dynamic code execution detected.

Critical
Code
suspicious.dynamic_code_execution
Location
scripts/vet.py:22