Back to skill

Security audit

Tuya Cloud

Security checks for vulnerabilities and agentic risk

Overview

This Tuya skill mostly does what it says, but it handles device-control secrets and physical valve actions in ways that need careful review before installation.

Install only if you understand that this skill can control real Tuya devices, including valves and switches. Use a restricted Tuya project, avoid running bundled tests against live devices, do not share logs/transcripts containing local_key values, keep .env out of source control, and prefer pinned dependencies and safer secret storage before using LAN control.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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
scripts/tuya_controller.py:434
Finding
Tuya Local Device Keys Are Exposed Through Process Arguments and Command Output## Vulnerability Details **File Location**: `scripts/tuya_controller.py:434-452, 535-546`; documented usage in `SKILL.md:165-183` **Vulnerability Type**: Plaintext exposure of device-control credentials **Risk Level**: High ### Vulnerable Code ```python if args.output_format == 'json': enriched = {} for gw_id, info in devices.items(): entry = dict(info) if gw_id in cloud_map: entry['name'] = cloud_map[gw_id].get('name') entry['local_key'] = cloud_map[gw_id].get('local_key') enriched[gw_id] = entry print(json.dumps(enriched, indent=2)) return print(f"\nFound {len(devices)} local device(s):\n") for gw_id, info in devices.items(): name = cloud_map.get(gw_id, {}).get('name', gw_id) local_key = cloud_map.get(gw_id, {}).get('local_key', '') ip = info.get('ip', '?') ver = info.get('version', '?') print(f" {name}") print(f" ID: {gw_id} IP: {ip} version: {ver}" + (f" local_key: {local_key}" if local_key else "")) ``` ```python p = sub.add_parser("read_local", help="Read device status directly over LAN") p.add_argument("device_id") p.add_argument("ip") p.add_argument("local_key") p.add_argument("--version", type=float, default=3.3, help="Protocol version (default: 3.3)") p.add_argument("--output_format", choices=["json", "text"], default="json") p.set_defaults(func=cmd_read_local) p = sub.add_parser("control_local", help="Control a device directly over LAN") p.add_argument("device_id") p.add_argument("ip") p.add_argument("local_key") p.add_argument("commands", help='JSON array, e.g. \'[{"dp":1,"value":true}]\' or \'[{"code":"switch_1","value":true}]\'') p.add_argument("--version", type=float, default=3.3, help="Protocol version (default: 3.3)") p.set_defaults(func=cmd_control_local) ``` ### Technical Analysis A Tuya `local_key` is a device-control credential used to authenticat ...[truncated 1601 chars]
Remediation
## Remediation Suggestions - Never include `local_key` values in standard text or JSON output. - Make enriched scan output return only non-sensitive fields such as name, device ID, IP address, and protocol version. - Obtain local keys from a protected environment variable, operating-system credential store, permission-restricted configuration file, or interactive hidden input. - Avoid placing secrets in positional or optional command-line arguments. - If credential export is indispensable, place it behind a separate explicit operation, display a strong warning, and write to a file created with owner-only permissions rather than stdout. - Redact known sensitive fields from logs, exceptions, Agent responses, and diagnostics. - Rotate any local keys that may already have appeared in shared logs or transcripts.

T09 · Insecure Skill Coding Practices

Error
Location
tests/test_water_valve.py:34
Finding
Automatically Discoverable Tests Activate Physical Water Valves Without a Reliable Safety Gate## Vulnerability Details **File Location**: `tests/test_water_valve.py:34-131` **Vulnerability Type**: Unsafe hardware integration test with physical side effects **Risk Level**: High ### Vulnerable Code ```python def test_left_water_valve_on_off(): """Switch the left water valve ON, then OFF.""" print("=" * 50) print("Test: Left water valve ON/OFF") print("=" * 50) print(f"Device (water valve): {WATER_VALVE_DEVICE_ID}") print(f"Gateway: {WATER_VALVE_GATEWAY_ID}") print(f"DP code (left valve): {LEFT_VALVE_DP}") print() client = load_tuya_client() print("✅ API client connected") print() # Optional: show current state before changing try: status = get_device_status(client, WATER_VALVE_DEVICE_ID) print("Current device status (before):", status) print() except Exception as e: print("⚠️ Could not read current status:", e) print() # Turn left valve ON print("Sending: left water valve ON...") try: send_device_commands( client, WATER_VALVE_DEVICE_ID, [{"code": LEFT_VALVE_DP, "value": True}], ) print("✅ Left water valve ON command sent") except Exception as e: print(f"❌ Failed to turn ON: {e}") return False time.sleep(2) # Turn left valve OFF print("Sending: left water valve OFF...") try: send_device_commands( client, WATER_VALVE_DEVICE_ID, [{"code": LEFT_VALVE_DP, "value": False}], ) print("✅ Left water valve OFF command sent") except Exception as e: print(f"❌ Failed to turn OFF: {e}") return False ``` The second test repeats the same unsafe pattern for the other channel: ```python # Turn right valve ON print("Sending: right water valve ON...") try: send ...[truncated 2458 chars]
Remediation
## Remediation Suggestions - Replace live cloud operations in ordinary unit tests with mocked `tinytuya` clients and assertions on generated requests. - Move hardware tests to a separately named integration-test suite excluded from default test discovery. - Require an explicit opt-in environment variable or command-line marker, such as `RUN_TUYA_HARDWARE_TESTS=1`. - Require confirmation of the target device and channel before any physical activation in an interactive environment. - Put the OFF operation in a `finally` block so cleanup is attempted regardless of intermediate failures. - Send a short device-side countdown together with the ON command as a failsafe. - After cleanup, query device status and fail the test if the channel is not confirmed OFF. - Add test-runner timeouts and emergency cleanup hooks, while recognizing that these do not replace a device-side auto-off mechanism. - Do not run physical-device tests from untrusted pull requests or shared CI workers.

T08 · Insecure Dependencies

Warning
Location
requirements.txt:1
Finding
Mutable Unpinned Dependencies Permit Unreviewed Supply-Chain Code## Vulnerability Details **File Location**: `requirements.txt:1-2` **Vulnerability Type**: Unpinned third-party dependencies without integrity verification **Risk Level**: Medium ### Vulnerable Code ```text tinytuya>=1.12.0 python-dotenv>=1.0.0 ``` The documentation instructs users to resolve these mutable dependencies directly: ```bash pip install tinytuya python-dotenv ``` ### Technical Analysis Lower-bound-only version constraints permit pip to select any future compatible release. The project contains no lock file, exact version constraints, or artifact hashes that bind installation to reviewed package contents. Python packages can execute code during installation and later execute arbitrary package code when imported. Consequently, compromise of an upstream package account, a malicious future release, or an unexpected incompatible release can change the effective code executed by the Skill without any modification to this repository. This finding does not establish that the currently named packages are malicious. The vulnerability is the absence of reproducible dependency and integrity controls. ### Attack Path 1. An attacker compromises an upstream release channel or publishes a malicious future version through a compromised maintainer account. 2. A user follows the documented installation command or runs `pip install -r requirements.txt`. 3. Pip selects the malicious version because it satisfies the `>=` constraint. 4. Attacker-controlled code executes during installation or when `tinytuya` or `dotenv` is imported. 5. The code runs with the privileges of the installing or executing user and can access the Skill's environment, including Tuya credentials. ### Impact Assessment Successful exploitation provides arbitrary Python-code execution under the account installing or running the Skill. This may expose Tuya access credentials and local device keys, permit unauthorized device control, modify use ...[truncated 179 chars]
Remediation
## Remediation Suggestions - Pin each direct dependency to a specifically reviewed version. - Generate and commit a reproducible lock file that includes transitive dependencies. - Use hash verification, such as pip requirements containing `--hash` entries and installation with `--require-hashes`. - Install packages only from the official configured package index over TLS. - Review release notes and dependency changes before updating locked versions. - Run dependency vulnerability and provenance checks in CI. - Install and execute the project in a dedicated virtual environment under a non-privileged account. - Avoid documenting unconstrained `pip install package-name` commands; direct users to the reviewed lock or hashed requirements file instead.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
Findings (15)

Credential Access

High
Category
Privilege Escalation
Content
```bash
TUYA_ACCESS_ID=<Client ID / Access ID shown here>
TUYA_ACCESS_SECRET=<Client Secret / Access Secret shown here>
TUYA_API_ENDPOINT=https://openapi.tuyaeu.com   # match to your region
```
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 load_tuya_client() -> tinytuya.Cloud:
    """Return an authenticated tinytuya Cloud client from .env credentials."""
    access_id = os.getenv("TUYA_ACCESS_ID")
    access_secret = os.getenv("TUYA_ACCESS_SECRET")
    endpoint = os.getenv("TUYA_API_ENDPOINT", "https://openapi.tuyaus.com")
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
endpoint = os.getenv("TUYA_API_ENDPOINT", "https://openapi.tuyaus.com")

    if not access_id or not access_secret:
        raise ValueError("TUYA_ACCESS_ID and TUYA_ACCESS_SECRET must be set in .env")

    region_map = {'tuyacn': 'cn', 'tuyaus': 'us', 'tuyaeu': 'eu', 'tuyain': 'in'}
    region = next((r for k, r in region_map.items() if k in endpoint), 'us')
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Description-Behavior Mismatch

High
Confidence
98% confidence
Finding
When local scan enrichment is enabled, the code fetches each device's local_key from the cloud map and prints it in both JSON and text output. The local_key is effectively a device secret for direct LAN control, so exposing it to stdout/logs can enable unauthorized control of devices by anyone with access to the terminal output, logs, or command history.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The README instructs users to store live Tuya cloud credentials in a .env file but does not warn that these values are sensitive secrets that must not be committed to source control or shared in logs. This increases the chance of accidental credential exposure, which could allow unauthorized access to device data and remote control of linked IoT devices.

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill advertises use of environment-backed credentials but does not declare an explicit tool scope such as permissions or allowed-tools. That increases the chance an agent can access sensitive secrets or invoke capabilities more broadly than intended, weakening least-privilege controls for a skill that can both read cloud data and send device-control commands.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The skill includes commands that can switch valves and other devices on or off, including timed activation, but it does not present a clear user-facing warning that these actions change physical device state. In an agent setting, missing safety disclosure can lead to unintended real-world actuation such as opening water valves or toggling switches without adequate confirmation or operator awareness.

Description-Behavior Mismatch

Medium
Confidence
95% confidence
Finding
The code includes local LAN discovery functionality that is broader than the skill description suggests. While device scanning is not inherently malicious, undocumented network discovery increases the attack surface and can surprise users by probing the local network in a way they may not have expected from a cloud-control skill.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The scan_local command can disclose sensitive device secrets without any safety warning or masking. Even though the leak occurs only with --enrich, the output path is designed to expose credentials directly, which can lead to credential disclosure through console logs, shell history capture, CI logs, or copied output.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The CLI accepts local_key as a positional argument and demonstrates its use in help examples, which encourages users to place secrets directly on the command line. Command-line arguments are commonly exposed via shell history, process listings, audit tools, and orchestration logs, making this an avoidable credential leakage risk.

Missing User Warnings

Low
Confidence
88% confidence
Finding
The local scan feature performs network discovery via UDP broadcast but does not warn users that it enumerates devices on the local network and may reveal device identities, IPs, and metadata. While this is a documented feature rather than covert behavior, lack of a privacy/discovery warning can cause unintentional reconnaissance in sensitive home or enterprise environments.

Unpinned Dependencies

Low
Category
Supply Chain
Content
tinytuya>=1.12.0
python-dotenv>=1.0.0
Confidence
94% confidence
Finding
The dependency is specified with only a lower bound, so builds may resolve to different versions over time and can silently pick up breaking changes or newly introduced vulnerable releases. In a skill that controls IoT devices, supply-chain instability matters because dependency compromise or regression could affect device access and command execution paths.

Unpinned Dependencies

Low
Category
Supply Chain
Content
tinytuya>=1.12.0
python-dotenv>=1.0.0
Confidence
95% confidence
Finding
python-dotenv is also unpinned, which makes installations non-reproducible and prevents verifying whether the resolved version is affected by known advisories. Because this skill relies on environment-based secrets, uncontrolled dependency resolution in configuration-loading code increases supply-chain and reliability risk.

Unverifiable Dependency: python-dotenv has 2 known advisory(ies) (CVE-2026-28684 (python-dotenv: Symlink following in set_key allows arbitrary file overwrite via ); CVE-2026-28684 (python-dotenv reads key-value pairs from a .env file and can set them as environ)), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
86% confidence
Finding
The manifest does not pin python-dotenv, and the package has known advisories, so there is no way to determine from this file whether deployments will install a fixed or affected version. This becomes more relevant in this skill because .env handling is central to loading Tuya API credentials, and a vulnerable dotenv implementation could contribute to unsafe file handling or secret-management issues depending on how it is used elsewhere.

Context-Inappropriate Capability

Low
Confidence
91% confidence
Finding
The configuration hard-codes real Tuya device identifiers and names for specific physical assets, including water valves and environmental sensors. Even though these are not API secrets by themselves, they expose the existence, purpose, and control targets of real-world infrastructure, which can aid unauthorized targeting, reconnaissance, and misuse when combined with valid credentials or other weaknesses.

Static analysis

No suspicious patterns detected.