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

Content
View full analysis

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

Content
View full analysis

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

Content

No source excerpt is available for this finding.

Missing User Warnings

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

Content

No source excerpt is available for this finding.

Missing User Warnings

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

Content

No source excerpt is available for this finding.

Missing User Warnings

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

Content

No source excerpt is available for this finding.

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.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.