Back to skill

Security audit

Dyson Fan Control

Security checks for vulnerabilities and agentic risk

Overview

This skill is coherent for controlling Dyson devices, but users should protect the saved device credentials and be careful with heater commands.

Install only in an isolated user environment, run setup only when you are comfortable entering Dyson account credentials, treat ~/.dyson/config.json as sensitive, restrict it to your user account, and confirm the target device before issuing heat commands.

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

T09 · Insecure Skill Coding Practices

Warning
Location
src/dyson_cli/config.py:10
Finding
Dyson MQTT credentials are stored without enforced restrictive permissions## Vulnerability Details **File Location**: `src/dyson_cli/config.py`, lines 10-28 **Vulnerability Type**: Plaintext sensitive-data storage with insufficient permission enforcement **Risk Level**: Medium ### Vulnerable Code ```python CONFIG_DIR = Path.home() / ".dyson" CONFIG_FILE = CONFIG_DIR / "config.json" def ensure_config_dir() -> Path: """Ensure the config directory exists.""" CONFIG_DIR.mkdir(parents=True, exist_ok=True) return CONFIG_DIR def load_config() -> dict: """Load configuration from disk.""" if not CONFIG_FILE.exists(): return {"devices": [], "default_device": None} return json.loads(CONFIG_FILE.read_text()) def save_config(config: dict) -> None: """Save configuration to disk.""" ensure_config_dir() CONFIG_FILE.write_text(json.dumps(config, indent=2)) ``` The sensitive value written through this function originates in `src/dyson_cli/cli.py`, lines 102-108: ```python for device in devices: device_info = { "name": device.name, "serial": device.serial, "credential": device.credential, "product_type": device.product_type, } config["devices"].append(device_info) ``` ### Technical Analysis Device MQTT credentials are deliberately persisted in `~/.dyson/config.json`, but the code does not explicitly assign restrictive permissions to either the configuration directory or the credential file. `Path.mkdir()` and `Path.write_text()` rely on the process umask and any permissions already present on the path. On a system with a permissive umask, inherited access-control entries, or an existing configuration file with unsafe permissions, other local users may be able to read the serial number, local IP address, and MQTT credential. Storing a local device credential is necessary for the declared local-control functionality, but allowing its confidentiality to depend entirely on ambient operatin ...[truncated 1544 chars]
Remediation
## Remediation Suggestions 1. Create `~/.dyson` with mode `0700` and verify its effective permissions after creation. 2. Create the credential file with mode `0600`; do not rely solely on the caller's umask. 3. When the file already exists, inspect its mode and ACLs. Correct unsafe permissions or refuse to load credentials until the user resolves them. 4. Write configuration atomically through a temporary file in the protected directory, set mode `0600`, flush and synchronize it, and then replace the destination with `os.replace()`. 5. Consider storing credentials in an operating-system secret store or keyring, leaving only non-sensitive device metadata in JSON. 6. Avoid printing credential values in logs or exception messages. 7. Document the sensitivity and required permissions of `~/.dyson/config.json`. A hardened implementation should use explicit permission controls, for example: ```python import json import os import tempfile def ensure_config_dir() -> Path: CONFIG_DIR.mkdir(parents=True, exist_ok=True, mode=0o700) os.chmod(CONFIG_DIR, 0o700) return CONFIG_DIR def save_config(config: dict) -> None: ensure_config_dir() fd, temporary_name = tempfile.mkstemp(dir=CONFIG_DIR) try: os.fchmod(fd, 0o600) with os.fdopen(fd, "w", encoding="utf-8") as stream: json.dump(config, stream, indent=2) stream.flush() os.fsync(stream.fileno()) os.replace(temporary_name, CONFIG_FILE) os.chmod(CONFIG_FILE, 0o600) except Exception: try: os.unlink(temporary_name) except FileNotFoundError: pass raise ```

T08 · Insecure Dependencies

Warning
Location
pyproject.toml:31
Finding
Installation and runtime dependencies are retrieved from mutable or unbounded sources## Vulnerability Details **File Location**: `pyproject.toml`, lines 31-35; additionally documented in `README.md`, line 27 **Vulnerability Type**: Unpinned software supply-chain dependencies **Risk Level**: Medium ### Vulnerable Code `pyproject.toml`, lines 31-35: ```toml dependencies = [ "libdyson-neon>=1.6.0", "click>=8.0", "rich>=13.0", ] ``` `README.md`, lines 25-28: ```bash pip install git+https://github.com/tmustier/dyson-cli.git ``` ### Technical Analysis The documented Git installation command does not identify a reviewed release tag or immutable commit. Consequently, its effective payload can change whenever the repository's default branch changes. Installation may execute upstream build-backend code and installs the package version present at retrieval time. Runtime dependencies use lower-bound-only constraints without a lock file, upper bounds, or integrity hashes. A later release satisfying these constraints can therefore be selected without having been included in the audited source. This reduces reproducibility and expands exposure to upstream compromise, malicious releases, dependency-account takeover, or incompatible future changes. The dependencies appear relevant to the declared functionality: `libdyson-neon` supplies account, discovery, and appliance communication APIs; Click and Rich provide CLI and output functionality. The concern is not that these capabilities are unnecessary, but that the project does not constrain them to immutable, reviewed artifacts. ### Attack Path **Mutable Git installation path:** 1. An attacker compromises the referenced upstream repository, its maintainer account, or the default branch. 2. The attacker modifies package or build code. 3. A user follows the README command without specifying a commit or immutable tag. 4. Pip retrieves the modified repository contents. 5. Malicious build or package code executes with the installing user's pr ...[truncated 1184 chars]
Remediation
## Remediation Suggestions 1. Change the README installation command to reference a signed release, immutable version, or full commit hash rather than the mutable default branch. 2. Publish reviewed release artifacts to a trusted package index and document installation by exact version. 3. Maintain a lock or constraints file containing exact transitive versions and cryptographic hashes. 4. Use `pip --require-hashes` or an equivalent reproducible dependency-management workflow for controlled deployments. 5. Review and test dependency upgrades before updating the lock file. 6. Add automated dependency vulnerability and provenance scanning. 7. Pin the build backend as well as runtime dependencies in release-building environments. 8. Recommend installation in an isolated virtual environment without administrator privileges. An immutable Git example would be: ```bash pip install "git+https://github.com/tmustier/dyson-cli.git@FULL_REVIEWED_COMMIT_HASH" ``` For packaged releases, prefer an exact reviewed version together with a hash-locked constraints file.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (5)

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The skill description presents the capability as local, same-WiFi MQTT control, but the documented behavior and referenced setup imply cloud login, OTP/password handling, device inventory retrieval, and persistent storage of credentials in local files. That mismatch is dangerous because users may invoke the skill expecting only local LAN actions while unknowingly exposing account credentials and authorizing broader account/device access than described.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The README explicitly documents storage of device credentials in `~/.dyson/config.json` but provides no warning to protect that file or restrict its permissions. Those credentials appear sufficient to control local Dyson devices, so disclosure to other local users, backups, or malware could enable unauthorized device access and reveal device/network details.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The skill documents heater activation and target temperature changes without any warning about physical-world safety, accidental activation, or the need to verify the device environment is safe. In an automation context, unattended or mistaken heater control can create burn, fire, or overheating risks, especially if triggered by ambiguous user requests or on the wrong device.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The setup command persists device credentials obtained from the Dyson account into a local config file without any visible protection, warning, or guidance on file permissions. If the config file is readable by other local users, included in backups, or accidentally committed/shared, those credentials could allow unauthorized control of the user's Dyson devices on the local network.

Known Vulnerable Dependency: click — 1 advisory(ies): CVE-2026-7246 (Pallets Click, versions 8.3.2 and below, contain a command injection vulnerabili)

High
Category
Supply Chain
Confidence
96% confidence
Finding
The project declares an unbounded vulnerable dependency on click with a version range of >=8.0, which can resolve to affected releases if the cited advisory is valid. Because click is a runtime dependency and this package exposes a CLI entry point, a command-injection flaw in click would directly affect normal use of the tool.

Static analysis

No suspicious patterns detected.