Back to skill

Security audit

ninebot-device-skill

Security checks for vulnerabilities and agentic risk

Overview

The skill matches its Ninebot vehicle-query purpose, but it handles a sensitive API key and vehicle location data with overly broad local configuration that could expose the key.

Install only if you are comfortable giving the skill access to your Ninebot vehicle data, including location. Prefer using the environment variable for the API key, avoid putting real keys in config.json or command-line arguments, and do not run it from a directory where an untrusted config.json could redirect authenticated requests.

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
scripts/ninebot_query.py:51
Finding
Configurable Network Destination Can Disclose the Ninebot API Key<![CDATA[ ## Vulnerability Details **File Location**: `scripts/ninebot_query.py`, lines 51-55, 103-129, and 157-160 **Vulnerability Type**: Unrestricted authenticated network destination **Risk Level**: High ### Complete Vulnerable Code ```python def get_base_url(cfg: Dict[str, Any], section: str) -> str: section_url = (cfg.get(section) or {}).get("base_url") if section_url: return section_url return cfg.get("base_url", "") ``` ```python def inject_api_key_header(cfg: Dict[str, Any], headers: Dict[str, str], api_key: str): auth_cfg = cfg.get("auth") or {} header_name = auth_cfg.get("api_key_header") or "x-api-key" prefix = auth_cfg.get("api_key_prefix") or "" headers[header_name] = f"{prefix}{api_key}" def list_devices(cfg: Dict[str, Any], api_key: str, lang: str): url = get_base_url(cfg, "devices").rstrip("/") + cfg["devices"]["path"] payload_tpl = cfg["devices"].get("payload") or {} payload = { k: (v.format(api_key=api_key, lang=lang) if isinstance(v, str) else v) for k, v in payload_tpl.items() } headers: Dict[str, str] = {} if api_key: inject_api_key_header(cfg, headers, api_key) payload_to_send = None if cfg["devices"]["method"].upper() == "GET" else payload res = http_request(cfg["devices"]["method"], url, headers=headers, payload=payload_to_send) devices = deep_get(res, cfg["devices"]["list_path"]) or [] return devices def get_device_info(cfg: Dict[str, Any], api_key: str, sn: str): path = cfg["device_info"]["path"].replace("{sn}", urllib.parse.quote(sn)) url = get_base_url(cfg, "device_info").rstrip("/") + path payload_tpl = cfg["device_info"].get("payload") or {} payload = { k: (v.format(api_key=api_key, sn=sn) if isinstance(v, str) else v) for k, v in payload_tpl.items() } headers: Dict[str, str] = {} if api_key: inject_api_key_header(cfg, headers, api_key) res = http_request(cfg["device_inf ...[truncated 2649 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pin authenticated requests to an explicit allowlist of approved Ninebot hosts, such as `cn-cbu-gateway.ninebot.com`. 2. Require the `https` scheme and reject plaintext HTTP destinations. 3. Normalize and validate the parsed hostname before attaching the API key. 4. Do not forward authorization headers if a redirect changes the request origin. 5. Remove per-section destination overrides unless they are operationally required. 6. If custom endpoints are required for development, require an explicit opt-in flag and display the destination before sending credentials. 7. Load configuration from a fixed, trusted location rather than implicitly trusting `config.json` in the current working directory. 8. Separate non-sensitive response-field mappings from security-sensitive settings such as destination, authentication header, and transport scheme. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/ninebot_query.py:151
Finding
API Key Can Be Exposed Through Plaintext Configuration and Process Arguments<![CDATA[ ## Vulnerability Details **File Location**: `scripts/ninebot_query.py`, lines 151-151; `config.example.json`, lines 1-3; `SKILL.md`, lines 42-55 and 112-127 **Vulnerability Type**: Insecure credential handling **Risk Level**: Medium ### Complete Vulnerable Code From `scripts/ninebot_query.py`: ```python parser.add_argument("--api-key", default=None, help="Ninebot device service API key") ``` From `config.example.json`: ```json { "apiKey": "your_ninebot_device_service_key_here" } ``` The script resolves the credential from command-line arguments, an environment variable, or the configuration object: ```python def resolve_api_key(cfg: Dict[str, Any], arg_api_key: Optional[str]) -> Optional[str]: if arg_api_key: return arg_api_key env_key = os.getenv("NINEBOT_DEVICESERVICE_KEY") if env_key: return env_key cfg_key = cfg.get("apiKey") if isinstance(cfg, dict) else None if cfg_key: return cfg_key return None ``` ### Technical Analysis The Skill supports storing the API key directly in a plaintext JSON file and accepts the secret through the `--api-key` command-line option. A command-line secret may be exposed through shell history, process inspection facilities, diagnostic tools, execution logs, or automation records. A plaintext `config.json` may be exposed through permissive file permissions, backups, synchronization software, accidental repository commits, or other local processes. The implementation does not check configuration-file permissions, integrate with an operating-system credential store, or warn users about command-line exposure. Although an environment variable is supported and preferred by the declared metadata, the less secure alternatives remain documented and available. ### Attack Path #### Command-Line Exposure 1. A user invokes the script with `--api-key` and a valid credential. 2. The command is retained in shell history or becomes visible through local process inspection ...[truncated 1029 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove or strongly discourage the `--api-key` option because command-line arguments are not an appropriate secret-transport mechanism. 2. Prefer controlled environment-variable injection or an operating-system secret manager. 3. If file-based credentials must remain supported: - Store them in a dedicated user configuration directory. - Require owner-only permissions before reading the file. - Refuse to load files writable or readable by untrusted users. - Clearly document that the file contains a secret. 4. Add `config.json` and other local secret files to `.gitignore`. 5. Separate credentials from ordinary API-mapping configuration. 6. Ensure errors, diagnostics, and Agent output never print the API key. 7. Consider accepting the key through a protected standard-input prompt when interactive entry is necessary. 8. Provide credential rotation guidance for users who suspect that a key has been exposed. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
scripts/ninebot_query.py:185
Finding
Multiple-Device Selection Output Discloses Unnecessary Upstream Metadata<![CDATA[ ## Vulnerability Details **File Location**: `scripts/ninebot_query.py`, lines 185-188 **Vulnerability Type**: Excessive sensitive-data disclosure **Risk Level**: Low ### Complete Vulnerable Code ```python else: # If multiple devices, return list for caller to decide if len(devices) > 1: print(json.dumps({"choose_device": devices}, ensure_ascii=False)) sys.exit(3) ``` ### Technical Analysis When more than one device is returned, the script prints each complete upstream device object. The documented selection workflow only requires the device serial number and display name, but the implementation does not project the response onto those fields. The API specification already shows that device records may contain additional fields such as image data. Future API versions could add further account or device metadata. Because the entire object is serialized, all such fields would be passed to the invoking Agent and could be retained in chat transcripts, execution logs, or downstream systems. This violates data-minimization principles: only the minimum information required to select a device should be emitted. ### Attack Path 1. The authenticated Ninebot account contains more than one vehicle. 2. The upstream device-list response includes fields beyond the serial number and device name. 3. The user runs the script without selecting a specific device. 4. The script serializes the complete `devices` collection. 5. Additional metadata is exposed to the calling Agent, terminal, logs, or downstream processing systems. No separate attacker-controlled code execution is required. Exploitation is primarily a privacy and data-handling concern arising during normal use. ### Impact Assessment The issue can disclose unnecessary vehicle or account metadata present in the upstream response. Known examples include device serial numbers, names, and image-related fields; the exact scope depends on the API response. The issue does not grant s ...[truncated 172 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Construct a strict selection response containing only fields required by the caller: ```python choices = [ { "sn": d.get(cfg["devices"]["sn_field"]), "name": d.get(cfg["devices"]["name_field"]), } for d in devices ] print(json.dumps({"choose_device": choices}, ensure_ascii=False)) ``` 2. Consider masking serial numbers unless their full values are needed to distinguish devices. 3. Define and enforce an output schema rather than forwarding upstream objects. 4. Review logs and Agent transcripts for unnecessary retention of vehicle identifiers. 5. Apply the same field-level allowlisting to error responses that currently include complete device records. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (9)

Lp3

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding
The skill declares access patterns that include environment variables, local file reads, and network use, but it does not explicitly constrain or disclose tool scope via permissions or allowed-tools. This weakens least-privilege protections and makes it easier for an agent runtime to over-grant capabilities beyond what is necessary for simple vehicle queries.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The skill explicitly supports retrieving precise vehicle location but provides no user-facing privacy warning, confirmation step, or guidance on safe handling of location data. Because location reveals sensitive real-world movement and presence information, exposing it without privacy safeguards increases the risk of stalking, surveillance, or unintended disclosure.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The skill tells users to provide and store an API key but does not warn that the key is a sensitive credential that can authorize access to vehicle data if exposed. Without handling guidance, users may paste the key into insecure channels or leave it in local files, increasing the risk of credential theft and unauthorized tracking or status queries.

Context-Inappropriate Capability

Medium
Confidence
92% confidence
Finding
The documentation instructs storing the Ninebot API key in a local config.json file, increasing the chance that a long-lived credential is left on disk in plaintext where other local processes, users, backups, or logs may access it. For a vehicle-information skill, persistent local storage of a sensitive token is not clearly necessary and expands the exposure window if the host is compromised.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The spec explicitly instructs users to supply a live API key via an environment variable or config file, but provides no warning about secret handling, least-privilege use, or avoiding disclosure in source control and logs. In an agent skill context, this can lead developers to embed production credentials insecurely, increasing the chance of credential leakage and unauthorized access to vehicle/device data.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The example configuration includes a plaintext `apiKey` field with a realistic placement that users are likely to copy verbatim into a file. Because this skill accesses sensitive vehicle telemetry such as location, battery, and status, insecure credential storage can expose both account access and downstream privacy-sensitive data if the config file is leaked.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
This code calls remote Ninebot APIs to list devices and fetch dynamic device information, including location data, using an API key. The script has no confirmation prompt or user-facing disclosure in code about transmitting identifiers and retrieving sensitive location/status information.

Natural-Language Policy Violations

Low
Confidence
76% confidence
Finding
Natural-language content in the file includes Chinese-only instructions and examples such as `使用...`, `无`, and Chinese location/time strings, but the document does not state that the skill is intentionally China-region-only or provide an alternative language option. That can create a language/locale policy issue when a skill implicitly forces one language without user opt-in.

Natural-Language Policy Violations

Low
Confidence
96% confidence
Finding
The CLI sets the default language to "zh", which imposes a specific locale unless the user explicitly overrides it. This is a natural-language policy concern because the skill defaults to one language rather than asking the user or using a neutral default.

Static analysis

No suspicious patterns detected.