Back to skill

Security audit

Ezviz Open PTZ Control

Security checks for vulnerabilities and agentic risk

Overview

This camera-control skill mostly does what it says, but it should be reviewed because it handles Ezviz credentials and tokens less safely than its documentation claims.

Install only if you are comfortable giving the skill Ezviz device-control authority. Use a dedicated low-permission Ezviz application, avoid passing AppSecret on the command line, assume tokens may be written to the shared temp cache even when caching is documented as disabled, and avoid shared machines unless the cache implementation is fixed or the token is cleaned up securely afterward.

Vulnerability Patterns
  • 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
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (3)

T09 · Insecure Skill Coding Practices

Error
Location
lib/token_manager.py:130
Finding
Disabling Token Caching Does Not Prevent Token Persistence<![CDATA[ ## Vulnerability Details **File Location**: `lib/token_manager.py`, lines 130-164 and 204-216 **Vulnerability Type**: Security control bypass resulting in plaintext bearer-token persistence **Risk Level**: High ### Complete Code Snippet ```python def get_cached_token(app_key, app_secret, use_cache=None): """ Get access token, using cached version if available and valid. """ # Check environment variable for cache override if use_cache is None: env_cache = os.environ.get("EZVIZ_TOKEN_CACHE", "1").strip().lower() use_cache = (env_cache not in ["0", "false", "no", "disable"]) cache_key = generate_cache_key(app_key, app_secret) # Try to load from cache first if use_cache: all_cache = load_token_cache() if cache_key in all_cache: cached = all_cache[cache_key] expire_time = cached.get("expire_time", 0) current_time = get_current_timestamp() # Check if cache is still valid (with buffer time) if current_time + TOKEN_BUFFER_TIME < expire_time: expire_str = time.strftime( '%Y-%m-%d %H:%M:%S', time.localtime(expire_time / 1000) ) print(f"[INFO] Using cached global token, expires: {expire_str}") return { "success": True, "access_token": cached["access_token"], "expire_time": expire_time, "from_cache": True } else: print("[INFO] Cached token expired or about to expire, will get new one") # Cache miss or expired, get new token return refresh_token(app_key, app_secret, cache_key) ``` ```python # Save to global cache all_cache = load_token_cache() all_cache[cache_key] = { "cache_key": cache_key, "access_token": access_token, "expire_time": expire_time, "created_at": get_current_timestamp(), ...[truncated 2104 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pass the cache decision into `refresh_token()` and conditionally suppress all cache operations: ```python def get_cached_token(app_key, app_secret, use_cache=None): if use_cache is None: env_cache = os.environ.get("EZVIZ_TOKEN_CACHE", "1").strip().lower() use_cache = env_cache not in ["0", "false", "no", "disable"] cache_key = generate_cache_key(app_key, app_secret) if use_cache: # Read and validate existing cache. ... return refresh_token( app_key, app_secret, cache_key=cache_key, save_to_cache=use_cache ) def refresh_token(app_key, app_secret, cache_key=None, save_to_cache=True): ... if save_to_cache: all_cache = load_token_cache() all_cache[cache_key] = { "cache_key": cache_key, "access_token": access_token, "expire_time": expire_time, "created_at": get_current_timestamp(), "app_key_prefix": app_key[:8] + "..." if len(app_key) > 8 else app_key } save_token_cache(all_cache) ``` 2. When caching is disabled, avoid creating the cache directory or reading any pre-existing cache. 3. Clearly distinguish between “do not read cache” and “do not persist token” if both behaviors must be supported. 4. Add automated tests confirming that `EZVIZ_TOKEN_CACHE=0` creates neither a cache directory nor a cache file. 5. Consider deleting an existing entry for the relevant account when the user explicitly disables persistence, subject to clear documentation and user consent. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/main.py:27
Finding
Documented Environment-Variable Authentication Is Ignored<![CDATA[ ## Vulnerability Details **File Location**: `scripts/main.py`, lines 27-28 and 218-240 **Vulnerability Type**: Credential exposure through command-line arguments **Risk Level**: Medium ### Complete Code Snippet ```python APP_KEY = os.getenv("EZVIZ_APP_KEY", "") APP_SECRET = os.getenv("EZVIZ_APP_SECRET", "") ``` ```python def main(): if len(sys.argv) < 4: print("Usage: python3 main.py appKey appSecret <command> [params...]") print("\nCommands:") print(" list - List all devices") print(" status <dev> - Get device status") print(" capacity <dev> - Get device capacity") print(" ptz_start <dev> <ch> <dir> <spd> - Start PTZ control") print(" ptz_stop <dev> <ch> - Stop PTZ control") print(" preset_add <dev> <ch> - Add preset") print(" preset_move <dev> <ch> <idx> - Move to preset") print(" preset_clear <dev> <ch> <idx> - Clear preset") print(" mirror <dev> <ch> <cmd> - Mirror flip") sys.exit(1) # Parse arguments app_key = sys.argv[1] app_secret = sys.argv[2] command = sys.argv[3] args = sys.argv[4:] ``` ### Technical Analysis The script reads `EZVIZ_APP_KEY` and `EZVIZ_APP_SECRET` into module-level variables but never uses those values. Instead, `main()` requires both credentials as positional command-line arguments. This behavior contradicts the Skill documentation, which identifies environment variables as the preferred and highest-priority credential source. Command-line arguments are commonly visible through process-inspection facilities, diagnostic tooling, shell history, orchestration logs, audit records, and job definitions. The problem is especially significant for the AppSecret because compromise of the long-lived application credential may allow an ...[truncated 1349 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Make environment variables the primary credential source: ```python def main(): app_key = os.getenv("EZVIZ_APP_KEY", "") app_secret = os.getenv("EZVIZ_APP_SECRET", "") if len(sys.argv) < 2: print("Usage: python3 main.py <command> [params...]") sys.exit(1) command = sys.argv[1] args = sys.argv[2:] if not app_key or not app_secret: print("[ERROR] Set EZVIZ_APP_KEY and EZVIZ_APP_SECRET") sys.exit(1) ``` 2. Remove positional secret arguments from the normal interface and from all examples. 3. If an alternative input method is required, use a protected credential file, standard input, a no-echo prompt through `getpass`, or an operating-system secret store. 4. If command-line credential compatibility must temporarily remain, require an explicit deprecated option and display a warning about process-list and shell-history exposure. 5. Update usage text and documentation so that the actual credential precedence exactly matches the implementation. 6. Rotate any AppSecret that may already have been exposed through shell history, logs, or process monitoring. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
lib/token_manager.py:42
Finding
Predictable Shared Temporary Cache Permits Symlink and Permission Attacks<![CDATA[ ## Vulnerability Details **File Location**: `lib/token_manager.py`, lines 42-51 and 88-109 **Vulnerability Type**: Unsafe temporary-file and shared-directory handling **Risk Level**: High ### Complete Code Snippet ```python def get_cache_dir(): """Get global cache directory path.""" base_temp = tempfile.gettempdir() cache_dir = os.path.join(base_temp, CACHE_DIR_NAME) os.makedirs(cache_dir, exist_ok=True) return cache_dir def get_cache_file_path(): """Get global cache file path.""" return os.path.join(get_cache_dir(), CACHE_FILE_NAME) ``` ```python def save_token_cache(cache_data): """ Save all tokens to global cache file (atomic write). Args: cache_data: Dict of all cached tokens """ cache_file = get_cache_file_path() cache_dir = get_cache_dir() try: # Write to temp file first, then rename (atomic operation) temp_file = cache_file + ".tmp" with open(temp_file, 'w') as f: json.dump(cache_data, f, indent=2) os.replace(temp_file, cache_file) # Set file permissions (readable only by owner) os.chmod(cache_file, 0o600) return True except Exception as e: print(f"[WARNING] Failed to save token cache: {e}", file=sys.stderr) return False ``` ### Technical Analysis The cache uses a fixed path under a shared temporary directory. The code accepts an existing `ezviz_global_token_cache` directory without checking whether it is owned by the current user, whether its permissions are safe, or whether it is a symbolic link. The temporary filename is also predictable: `global_token_cache.json.tmp`. Opening it with ordinary write mode follows symbolic links and does not require exclusive creation. The file mode is inherited from the process umask, and restrictive mode `0600` is applied only after the temporary file has been written and renamed. `os.replace()` provides atomic replacement, but it does not make ...[truncated 1717 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Use a per-user cache location rather than a globally predictable shared directory. Prefer the platform-specific user cache directory or an operating-system credential store. 2. Create the cache directory with mode `0700` and verify its ownership and type before every use: ```python os.makedirs(cache_dir, mode=0o700, exist_ok=True) st = os.lstat(cache_dir) if not stat.S_ISDIR(st.st_mode): raise RuntimeError("Cache path is not a directory") if st.st_uid != os.getuid(): raise RuntimeError("Cache directory is not owned by the current user") if stat.S_IMODE(st.st_mode) != 0o700: os.chmod(cache_dir, 0o700) ``` 3. Reject symbolic links for the cache directory and destination file. 4. Replace the predictable `.tmp` file with securely and exclusively created temporary storage: ```python fd, temp_path = tempfile.mkstemp( prefix=".global_token_cache.", dir=cache_dir, text=True ) try: os.fchmod(fd, 0o600) with os.fdopen(fd, "w") as f: json.dump(cache_data, f, indent=2) f.flush() os.fsync(f.fileno()) os.replace(temp_path, cache_file) finally: if os.path.exists(temp_path): os.unlink(temp_path) ``` 5. Validate the existing cache file with `lstat()` before reading it, including ownership, regular-file type, and restrictive permissions. 6. Set restrictive permissions at creation time rather than after sensitive content has been written. 7. Add tests that pre-create malicious directories, cache-file symlinks, and temporary-file symlinks and verify that the implementation fails safely. ]]>
Vulnerability Patterns
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (30)

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The skill emphasizes environment-variable credentials, but the finding indicates the main entry point accepts secrets on the command line and exposes additional undeclared device-control features such as capacity and mirror operations. Passing secrets via argv is risky because they may be exposed through shell history, process listings, or logs, and undeclared control functions expand operational impact without clear review.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The skill emphasizes environment-variable credentials, but the finding indicates the main entry point accepts secrets on the command line and exposes additional undeclared device-control features such as capacity and mirror operations. Passing secrets via argv is risky because they may be exposed through shell history, process listings, or logs, and undeclared control functions expand operational impact without clear review.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
cat /tmp/ezviz_global_token_cache/global_token_cache.json

# 清除缓存
rm -rf /tmp/ezviz_global_token_cache/
```

### 验证命令
Confidence
90% 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
cat /tmp/ezviz_global_token_cache/global_token_cache.json

# 清除缓存
rm -rf /tmp/ezviz_global_token_cache/
```

### 验证命令
Confidence
90% 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
cat /tmp/ezviz_global_token_cache/global_token_cache.json

# 清除缓存
rm -rf /tmp/ezviz_global_token_cache/
```

### 验证命令
Confidence
90% 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
cat /tmp/ezviz_global_token_cache/global_token_cache.json

# 清除缓存
rm -rf /tmp/ezviz_global_token_cache/
```

### 验证命令
Confidence
90% 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).

Credential Access

High
Category
Privilege Escalation
Content
### 2. 环境变量安全

```bash
# 推荐:使用 .env 文件(不要提交到版本控制)
echo "EZVIZ_APP_KEY=your_key" >> .env
echo "EZVIZ_APP_SECRET=your_secret" >> .env
chmod 600 .env
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
### 2. 环境变量安全

```bash
# 推荐:使用 .env 文件(不要提交到版本控制)
echo "EZVIZ_APP_KEY=your_key" >> .env
echo "EZVIZ_APP_SECRET=your_secret" >> .env
chmod 600 .env
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
### 2. 环境变量安全

```bash
# 推荐:使用 .env 文件(不要提交到版本控制)
echo "EZVIZ_APP_KEY=your_key" >> .env
echo "EZVIZ_APP_SECRET=your_secret" >> .env
chmod 600 .env
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
### 2. 环境变量安全

```bash
# 推荐:使用 .env 文件(不要提交到版本控制)
echo "EZVIZ_APP_KEY=your_key" >> .env
echo "EZVIZ_APP_SECRET=your_secret" >> .env
chmod 600 .env
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
### 2. 环境变量安全

```bash
# 推荐:使用 .env 文件(不要提交到版本控制)
echo "EZVIZ_APP_KEY=your_key" >> .env
echo "EZVIZ_APP_SECRET=your_secret" >> .env
chmod 600 .env
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
```bash
# 清除所有缓存
rm -rf /tmp/ezviz_global_token_cache/

# 或使用 CLI
python3 token_manager.py clear
Confidence
90% confidence
Finding
Although the command targets a specific cache directory, the README recommends recursive force deletion in a temp path. If copied into scripts or modified carelessly, this pattern can lead to unintended data loss, especially on shared systems or if variables/path assumptions are later introduced around the deletion logic.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
```bash
# 清除所有缓存
rm -rf /tmp/ezviz_global_token_cache/

# 或使用 CLI
python3 token_manager.py clear
Confidence
90% confidence
Finding
Although the command targets a specific cache directory, the README recommends recursive force deletion in a temp path. If copied into scripts or modified carelessly, this pattern can lead to unintended data loss, especially on shared systems or if variables/path assumptions are later introduced around the deletion logic.

Credential Access

High
Category
Privilege Escalation
Content
def get_cached_token(app_key, app_secret, use_cache=None):
    """
    Get access token, using cached version if available and valid.
    
    Args:
        app_key: Ezviz app key
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def get_cached_token(app_key, app_secret, use_cache=None):
    """
    Get access token, using cached version if available and valid.
    
    Args:
        app_key: Ezviz app key
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def get_cached_token(app_key, app_secret, use_cache=None):
    """
    Get access token, using cached version if available and valid.
    
    Args:
        app_key: Ezviz app key
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def get_cached_token(app_key, app_secret, use_cache=None):
    """
    Get access token, using cached version if available and valid.
    
    Args:
        app_key: Ezviz app key
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def get_cached_token(app_key, app_secret, use_cache=None):
    """
    Get access token, using cached version if available and valid.
    
    Args:
        app_key: Ezviz app key
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def get_cached_token(app_key, app_secret, use_cache=None):
    """
    Get access token, using cached version if available and valid.
    
    Args:
        app_key: Ezviz app key
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
result = get_cached_token(args.app_key, args.app_secret, use_cache=use_cache)
        
        if result["success"]:
            print(f"\nAccess Token: {result['access_token'][:30]}...")
            print(f"Expires: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(result['expire_time'] / 1000))}")
            print(f"From Cache: {result['from_cache']}")
        else:
Confidence
97% confidence
Finding
The CLI prints the first 30 characters of the access token to stdout. Partial token disclosure in logs, terminals, CI output, or agent transcripts can materially aid credential compromise, especially if token formats are predictable or if logs are broadly accessible; in this skill context, the token grants cloud-device control, making leakage more dangerous.

Lp3

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding
The skill documents effective capabilities including environment-variable access, local file read/write, network access, and shell execution, but does not declare an explicit tool scope such as permissions or allowed-tools. This weakens policy enforcement and reviewability because an agent may be able to invoke more powerful operations than a consumer expects from the metadata alone.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
# 推荐:使用 .env 文件(不要提交到版本控制)
echo "EZVIZ_APP_KEY=your_key" >> .env
echo "EZVIZ_APP_SECRET=your_secret" >> .env
chmod 600 .env

# 加载环境变量
source .env
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Natural-Language Policy Violations

Medium
Confidence
92% confidence
Finding
The skill description and all operational guidance are written in Chinese, with no indication that users may choose another language or that the skill is intentionally limited to a Chinese-speaking or region-specific audience. Under the language/locale policy, forcing a specific language without user opt-in is a natural-language policy violation.

Context-Inappropriate Capability

Medium
Confidence
86% confidence
Finding
The skill manifest describes controlling Ezviz PTZ devices, querying device status, and managing presets. This documentation introduces a separate command-line token manager with commands to get, refresh, list, and clear shared tokens, which is an account/credential-management capability rather than an obvious end-user PTZ control feature.

Context-Inappropriate Capability

Medium
Confidence
91% confidence
Finding
The README documents commands to list all cached tokens and clear all cached tokens across accounts, which exceeds the least-privilege needs of a PTZ/device-control skill. In a shared host environment, such cross-account cache administration can enable unintended visibility into other tenants' token metadata or denial of service by deleting valid cached credentials for unrelated accounts.

Static analysis

No suspicious patterns detected.