Back to skill

Security audit

Ezviz Open Restaurant Inspection

Security checks for vulnerabilities and agentic risk

Overview

The skill’s restaurant camera workflow is mostly disclosed and purpose-aligned, but it has material review concerns around automatic high-impact remote actions and insecure token persistence.

Review this skill before installing on any host with real camera access. Use a dedicated Ezviz app with minimal permissions and non-production devices first, assume images will leave the device for Ezviz AI analysis, and avoid multi-user or shared /tmp environments until token caching is fixed. Clear /tmp/ezviz_global_token_cache after testing, and do not rely on EZVIZ_TOKEN_CACHE=0 to prevent token persistence in this version.

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

T09 · Insecure Skill Coding Practices

Warning
Location
lib/token_manager.py:139
Finding

Token caching cannot actually be disabled

Content
View full analysis

Vulnerability Details

File Location: lib/token_manager.py, lines 139-170 and 220-232
Vulnerability Type: Security control bypass resulting in persistent plaintext bearer-token storage
Risk Level: Medium

Technical Analysis

The EZVIZ_TOKEN_CACHE=0 setting only prevents reading an existing token from the cache. On a cache miss—or whenever caching is explicitly disabled—get_cached_token() still invokes refresh_token(), which unconditionally loads and writes the global token cache.

Relevant code:

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

        if current_time + TOKEN_BUFFER_TIME < expire_time:
            return {
                "success": True,
                "access_token": cached["access_token"],
                "expire_time": expire_time,
                "from_cache": True
            }

# Cache miss or expired, get new token
return refresh_token(app_key, app_secret, cache_key)

The refresh path then writes the token regardless of the selected cache policy:

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(),
    "app_key_prefix": app_key[:8] + "..." if len(app_key) > 8 else app_key
}

if save_token_cache(all_cache):
    print(f"[INFO] Token saved to global cache")

This directly contra ...[truncated 1807 chars]

Remediation
View remediation

Remediation Suggestions

Propagate the selected cache policy into the refresh operation and completely skip cache reads and writes when caching is disabled. For example:

python
def get_cached_token(app_key, app_secret, use_cache=None):
    if use_cache is None:
        value = os.environ.get("EZVIZ_TOKEN_CACHE", "1").strip().lower()
        use_cache = value not in {"0", "false", "no", "disable"}

    cache_key = generate_cache_key(app_key, app_secret)

    if use_cache:
        # Perform the existing cache lookup.
        ...

    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(),
        }
        save_token_cache(all_cache)

Add an automated test that sets EZVIZ_TOKEN_CACHE=0, performs a mocked successful refresh, and verifies that neither the cache directory nor cache file is created. Remove any previously cached token when an operator switches to no-cache mode, or clearly document that existing entries must be cleared separately.

T09 · Insecure Skill Coding Practices

Error
Location
lib/token_manager.py:42
Finding

Predictable shared temporary cache permits local token exposure and file-race attacks

Content
View full analysis

Vulnerability Details

File Location: lib/token_manager.py, lines 42-54 and 93-116
Vulnerability Type: Unsafe temporary directory and predictable temporary-file handling
Risk Level: High

Technical Analysis

The token manager stores reusable bearer tokens under a fixed path in the system-wide temporary directory:

python
CACHE_DIR_NAME = "ezviz_global_token_cache"
CACHE_FILE_NAME = "global_token_cache.json"

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)

Cache writes also use a predictable .tmp pathname and ordinary open(..., 'w'):

python
def save_token_cache(cache_data):
    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

Although the final file is changed to mode 0600, several protections are missing:

  • The cache directory is created at a predictable /tmp path without verifying its owner, type, or permissions.
  • exist_ok=True accepts an object pre-created by another local user.
  • The directory is not explicitly restricted to mode 0700.
  • The predictable .tmp file is opened without exclusive creation or symlink protection.
  • The temporary ...[truncated 2814 chars]
Remediation
View remediation

Remediation Suggestions

Replace the shared predictable path with a user-private cache location, such as an appropriate platform-specific user cache directory. If a temporary directory must be used:

  1. Create a per-user directory with mode 0700.
  2. Include the effective user ID in the directory name or use tempfile.mkdtemp().
  3. Before every use, call lstat() and verify that the directory is a real directory, is owned by the effective user, and is not writable by group or others.
  4. Reject symbolic links and unexpected file types.
  5. Create temporary files securely in the verified directory with tempfile.mkstemp(), mode 0600, and close-on-exec enabled.
  6. Flush and fsync() the file before atomically replacing the cache.
  7. Use a lock file with the same ownership and symlink protections to serialize readers and writers.
  8. Set restrictive permissions at creation time rather than relying on a later chmod().

A hardened outline is:

python
cache_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
st = cache_dir.lstat()

if not stat.S_ISDIR(st.st_mode):
    raise SecurityError("Cache path is not a directory")
if st.st_uid != os.geteuid():
    raise SecurityError("Cache directory has an unexpected owner")
if st.st_mode & 0o077:
    raise SecurityError("Cache directory permissions are too broad")

fd, temp_path = tempfile.mkstemp(
    prefix=".global_token_cache.",
    dir=str(cache_dir),
    text=True,
)
try:
    os.fchmod(fd, 0o600)
    with os.fdopen(fd, "w", encoding="utf-8") as stream:
        json.dump(cache_data, stream)
        stream.flush()
        os.fsync(stream.fileno())
    os.replace(temp_path, cache_file)
finally:
    if os.path.exists(temp_path):
        os.unlink(temp_path)

Where available, use an operating-system credential store instead of storing bearer tokens in plaintext JSON. Add multi-user and malicious-precreated-directory tests to ensure the implemen ...[truncated 20 chars]

Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (20)

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding

The skill description omits important behaviors such as listing intelligent agents, creating a new agent from a template, reading local credentials from ~/.openclaw JSON files, and using a shared token cache under /tmp. In a security-sensitive skill dealing with cameras and cloud APIs, these undeclared behaviors materially increase exposure by modifying remote state, accessing local secrets, and persisting auth data beyond a single run.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

The skill description omits important behaviors such as listing intelligent agents, creating a new agent from a template, reading local credentials from ~/.openclaw JSON files, and using a shared token cache under /tmp. In a security-sensitive skill dealing with cameras and cloud APIs, these undeclared behaviors materially increase exposure by modifying remote state, accessing local secrets, and persisting auth data beyond a single run.

Content

No source excerpt is available for this finding.

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 109)May include surrounding context.

md
for f in ['SKILL.md', 'scripts/restaurant_inspection.py', 'lib/token_manager.py']:

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

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

Content

Scanner excerpt · lib/token_manager.py (reported line 121)May include surrounding context.

python
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

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

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

Content

Scanner excerpt · lib/token_manager.py (reported line 174)May include surrounding context.

python
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

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

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

Content

Scanner excerpt · lib/token_manager.py (reported line 190)May include surrounding context.

python
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

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

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

Content

Scanner excerpt · scripts/restaurant_inspection.py (reported line 123)May include surrounding context.

python
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

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

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

Content

Scanner excerpt · scripts/restaurant_inspection.py (reported line 454)May include surrounding context.

python
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

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

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

Content

Scanner excerpt · scripts/restaurant_inspection.py (reported line 457)May include surrounding context.

python
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

Credential Access

High
Category
Privilege Escalation
Confidence
98% confidence
Finding

The CLI prints the first 30 characters of the access token to stdout. Even partial token disclosure is sensitive because terminals, shell history, CI logs, process supervisors, and centralized logging may capture the output, enabling token theft or making brute-force/identification attacks easier in environments where the full token may also be exposed elsewhere.

Content

Scanner excerpt · lib/token_manager.py (reported line 362)May include surrounding context.

python
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:

Credential Access

High
Category
Privilege Escalation
Confidence
91% confidence
Finding

The skill retrieves credentials from environment variables and local OpenClaw config files, then uses them to obtain a shared cached access token. This is sensitive credential access, and in the skill context it is more dangerous because the same script also performs remote actions and uses a global token cache, increasing the blast radius if the environment or cache is compromised.

Content

Scanner excerpt · scripts/restaurant_inspection.py (reported line 125)May include surrounding context.

python
def get_access_token(app_key, app_secret):
    """Get access token using global token manager (with caching)"""
    print("=" * 70)
    print("[Step 1] Getting access token...")
    
    # Use global token manager (handles caching automatically)
    token_result = get_cached_token(app_key, app_secret)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding

The skill advertises significant capabilities including environment access, filesystem access, network activity, and shell execution, but does not declare an explicit tool scope such as permissions or allowed-tools. This weakens least-privilege controls and makes it harder for reviewers or runtime policy systems to constrain what the skill can do, especially given that it handles credentials, local config files, token caches, and remote API calls.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
95% confidence
Finding

The skill persists authentication material in a global token cache under /tmp, creating session persistence beyond a single execution. Even with 0600 permissions, /tmp-based shared persistence increases the chance of token reuse across contexts, stale credential exposure, accidental cross-user/process interactions, and misuse if the host is compromised or if cache lifecycle controls are weak.

Content

Scanner excerpt · SKILL.md (reported line 14)May include surrounding context.

md
warnings:
      - "Use dedicated Ezviz credentials (not main account)"
      - "Queries intelligent agent list"
      - "May create agent from template"
      - "Captures device images"
      - "Sends images to aidialoggw.ys7.com"
      - "Token cached in /tmp/ezviz_global_token_cache/ (600)"

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

This file includes user-facing natural-language documentation in Chinese, which effectively forces a specific language for users of the skill. The policy allows fixed locale/language only when there is explicit opt-in or a clearly justified region-specific constraint, neither of which is stated here.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
75% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · scripts/restaurant_inspection.py (reported line 170)May include surrounding context.

python
agents = []
        # Handle both response formats
        if 'data' in result:
            if isinstance(result['data'], dict) and 'appList' in result['data']:
                agents = result['data']['appList']
            elif isinstance(result['data'], list):
                agents = result['data']

Session Persistence

Medium
Category
Rogue Agent
Confidence
75% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · scripts/restaurant_inspection.py (reported line 171)May include surrounding context.

python
agents = []
        # Handle both response formats
        if 'data' in result:
            if isinstance(result['data'], dict) and 'appList' in result['data']:
                agents = result['data']['appList']
            elif isinstance(result['data'], list):
                agents = result['data']

Session Persistence

Medium
Category
Rogue Agent
Confidence
93% confidence
Finding

The script can create a new persistent remote intelligent agent from a template in the user's Ezviz account. This is a real side effect beyond transient analysis: it modifies remote account state and leaves assets behind, which can increase attack surface, incur costs, or create unnoticed long-lived integrations if run unintentionally or by another local process.

Content

Scanner excerpt · scripts/restaurant_inspection.py (reported line 211)May include surrounding context.

python
return None

def copy_agent_template(access_token, template_id):
    """Copy agent template to create new agent"""
    print("[INFO] Creating new restaurant agent from template...")
    
    headers = {

External Transmission

Medium
Category
Data Exfiltration
Confidence
97% confidence
Finding

The skill sends captured device images to a remote AI endpoint for analysis, which is an external transmission of potentially sensitive surveillance data. Although the script discloses this behavior, it still creates real privacy and data-exposure risk because images from restaurant or camera environments may contain people, interiors, or other sensitive content and are transmitted off-device to a third-party service.

Content

Scanner excerpt · scripts/restaurant_inspection.py (reported line 313)May include surrounding context.

python
}
    
    try:
        response = requests.post(ANALYSIS_URL, headers=headers, json=payload, timeout=60)
        result = response.json()
        
        if result.get('meta', {}).get('code') == 200:

Vague Triggers

Low
Category
Not specified by scanner
Confidence
91% confidence
Finding

This markdown file describes the skill's purpose but does not specify how or when the skill should be invoked, what phrases or contexts activate it, or any exclusion conditions. In a skill catalog or manifest-like description, that lack of trigger specificity can cause overly broad or unintended invocation decisions by an orchestrator.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
98% confidence
Finding

The pre-run verification text tells users to verify that the code uses only open.ys7.com, while the same skill declares aidialoggw.ys7.com as a legitimate AI-analysis endpoint. This inconsistency can mislead reviewers into performing incomplete domain validation and normalizes contradictory security instructions in a skill that already performs remote image and token handling.

Content

No source excerpt is available for this finding.