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

Content
View full analysis

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
View 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

Content
View full analysis

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
View 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

Content
View full analysis

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
View 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
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/vet.py (reported line 38)May include surrounding context.

python
(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),

Undeclared Tool Scope

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.

Content

No source excerpt is available for this finding.

Unrestricted Tool Access

Medium
Category
Excessive Agency
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.

Content

Scanner excerpt · scripts/vet.py (reported line 22)May include surrounding context.

python
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),

Unrestricted Tool Access

Medium
Category
Excessive Agency
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.

Content

Scanner excerpt · scripts/vet.py (reported line 23)May include surrounding context.

python
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),

Missing User Warnings

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.