Back to skill

Security audit

Grid Notice Voice Pack

Security checks for vulnerabilities and agentic risk

Overview

The skill can produce the promised voice clips, but it also grants broad account powers, sends installation/device metadata, and silently self-updates installed code by default.

Install only if you are comfortable granting a shared Beatra device token with broad media, artifact, task, and wallet capabilities and with the package silently replacing its own files by default. Before use, consider disabling auto-updates with the documented `update --auto off` command, use a Beatra account with limited funds, and only upload voice samples or local files you explicitly intend to send to Beatra.

Vulnerability Patterns
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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
Findings (6)

T03 · Remote Payload Retrieval and Execution

Error
Location
scripts/mcp_client.py:518
Finding
Silent unsigned self-update creates a post-review remote code execution channel<![CDATA[ ## Vulnerability Details **File Location**: `scripts/mcp_client.py:518-525, 931-1016, 1529-1532`; `SKILL.md:185-199`; `references/automatic-updates-and-safety.md:3-7` **Vulnerability Type**: Silent remote executable replacement without independent signature verification **Risk Level**: Critical ### Vulnerable Code ```python def _read_update_state(update_home: Path) -> dict[str, Any]: path = update_home / "state.json" try: value = json.loads(path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): return {"schema_version": 1, "auto_update": True} if not isinstance(value, dict) or value.get("schema_version") != 1: return {"schema_version": 1, "auto_update": True} return value ``` ```python checked = check_update(get_bytes=get_bytes) if not checked["update_available"]: return False _ensure_owned_baseline( install_root=resolved_root, update_home=update_home, get_bytes=get_bytes, ) discovery = checked["discovery"] manifest, new_files = download_update(discovery, get_bytes=get_bytes) _apply_update( install_root=resolved_root, update_home=update_home, discovery=discovery, manifest=manifest, new_files=new_files, ) return True ``` ```python if args.command == "update": ... else: maybe_auto_update() ``` The package documentation explicitly confirms this behavior: ```text The bundled client checks for a newer release for the installed package channel before ordinary Beatra commands, at most once every 24 hours. The check is silent and enabled by default... ``` ### Technical Analysis The client silently checks for updates before ordinary commands, enables automatic installation by default, downloads package files, and replaces local executable files. The updater validates archive and file hashes, but the expected hashes are obtained from mutable metadata served by the same remote publisher infrastructure. No pinned public key, detached signature, or ind ...[truncated 1791 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Disable automatic installation by default. Update only after explicit, informed user approval. 2. Sign discovery metadata and release manifests with a dedicated offline signing key. 3. Pin the corresponding public key in the audited package and verify signatures before trusting version numbers, URLs, or hashes. 4. Consider a transparency log or reproducible release mechanism to make unauthorized releases detectable. 5. Separate update checking from installation and display the proposed version, signed digest, changed files, and source before installation. 6. Avoid allowing the updater to replace itself directly. Use a separately reviewed and narrowly privileged installer. 7. Preserve the existing archive validation, destination checks, transactional rollback, and downgrade prevention as defense-in-depth controls. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/authorize.py:32
Finding
Authorization scopes and generic tool invocation exceed the voice-generation task<![CDATA[ ## Vulnerability Details **File Location**: `scripts/authorize.py:32-35`; `scripts/mcp_client.py:1464-1479, 1497-1500` **Vulnerability Type**: Excessive OAuth scope and unrestricted remote tool dispatch **Risk Level**: High ### Vulnerable Code ```python SCOPE = ( "mcp:tools artifacts:write images:generate videos:generate music:generate " "speech:generate voices:read voices:write wallet:spend tasks:read artifacts:read tasks:cancel" ) ``` ```python def _run_command(command: str, tool_name: str | None = None) -> dict[str, Any]: session = _session_with_registration( state_dir=Path.home() / ".beatra", post_json=_default_post_json, ) if command == "tools": return session.request(2, "tools/list", {}) try: arguments = json.load(os.sys.stdin) except json.JSONDecodeError as exc: raise RuntimeError("Tool arguments on stdin must be one JSON object") from exc if not isinstance(arguments, dict): raise RuntimeError("Tool arguments on stdin must be one JSON object") assert tool_name is not None return session.request( 2, "tools/call", {"name": tool_name, "arguments": arguments}, ) ``` ```python call = subparsers.add_parser("call", help="Call one tool with a JSON object on stdin") call.add_argument("tool_name") ``` ### Technical Analysis The declared Skill function is producing grid-notice speech clips and optionally cloning an authorized voice. Nevertheless, authorization requests image, video, and music generation permissions, wallet-spending access, artifact writes, task cancellation, and generic MCP tool access. The command-line client accepts an arbitrary `tool_name` and forwards it to `tools/call`. It contains no package-specific allowlist restricting calls to the operations documented as necessary for this Skill. As a result, the local client does not enforce the intended functional boundary. A shared full-scope bearer token magnifies the i ...[truncated 1053 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Issue package-specific, least-privilege credentials rather than a shared full-scope Device Token. 2. Restrict authorization to required operations, such as speech generation, authorized voice cloning, voice/model listing, necessary artifact upload/read, task status, and narrowly justified cancellation. 3. Remove image, video, music, and unrestricted wallet permissions unless a concrete declared feature requires them. 4. Add a strict local allowlist of permitted tool names and reject every unrecognized tool before sending a request. 5. Apply server-side authorization rules binding the package identity to its approved tools. 6. Separate read-only wallet inspection from spending authority where supported. 7. Display the exact requested capabilities to the user during authorization. ]]>

other

Warning
Location
scripts/authorize.py:347
Finding
Authorization transmits local hostname and Agent-platform fingerprint<![CDATA[ ## Vulnerability Details **File Location**: `scripts/authorize.py:347-368, 459-471`; `scripts/mcp_client.py:1127-1158, 1199-1209` **Vulnerability Type**: Environment reconnaissance and persistent device telemetry **Risk Level**: Medium ### Vulnerable Code ```python def detect_host_platform(explicit: str | None = None) -> str: if explicit: candidate = explicit.strip().lower().replace(" ", "-") if _PLATFORM_VALUE.fullmatch(candidate): return candidate env = os.environ if env.get("CLAUDECODE") == "1" or "CLAUDE_CODE_ENTRYPOINT" in env: return "claude-code" if any(key.startswith("CODEX_") for key in env): return "codex" ai_agent = env.get("AI_AGENT", "").lower() matched = re.match(r"([a-z0-9-]+)_", ai_agent) if matched and _PLATFORM_VALUE.fullmatch(matched.group(1)): return matched.group(1) return "unknown" ``` ```python def device_display_name() -> str | None: """A hostname the user will recognise in the console device list.""" try: name = socket.gethostname().strip() except OSError: return None if not name or not name.isprintable(): return None return name[:120] ``` ```python form: dict[str, str] = { "client_id": CLIENT_ID, "resource": MCP_URL, "scope": SCOPE, "platform": host_platform, "client_name": PACKAGE_DISPLAY_NAME, "external_installation_ref": external_reference, "package_version": PACKAGE_VERSION, "package_slug": PACKAGE_SLUG, } if device_name: form["device_name"] = device_name status, created = post_form(DEVICE_AUTHORIZATION_URL, form) ``` ### Technical Analysis The authorization helper inspects process-environment signatures to identify the hosting Agent and reads the system hostname through `socket.gethostname()`. It then sends these values with a stable installation reference and package metadata to the remote authorization service. Platform attribution is described in the ...[truncated 1324 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove hostname collection unless it is strictly required. 2. Use a random, non-identifying device label or ask the user to provide an optional display name. 3. Make Agent-platform telemetry opt-in rather than automatic. 4. Clearly disclose every transmitted field before authorization, including hostname, platform, stable installation reference, package slug, and version. 5. Minimize retention and prevent telemetry fields from being used as authorization or billing identifiers. 6. Provide a configuration option that disables all nonessential telemetry without disabling the Skill's core operation. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/mcp_client.py:231
Finding
Server-selected upload grants can exfiltrate local files to any HTTPS hostname<![CDATA[ ## Vulnerability Details **File Location**: `scripts/mcp_client.py:231-264` **Vulnerability Type**: Missing destination allowlist for local-file uploads **Risk Level**: Medium ### Vulnerable Code ```python def _complete_upload( result: dict[str, Any], *, mime_type: str, content: bytes, put_bytes: PutBytes, ) -> dict[str, str]: structured = result.get("structuredContent") instruction = structured.get("upload") if isinstance(structured, dict) else None if not isinstance(instruction, dict) or instruction.get("method") != "PUT": raise RuntimeError("Beatra upload instructions are invalid") url = instruction.get("url") headers = instruction.get("headers") if not isinstance(url, str) or not isinstance(headers, dict): raise RuntimeError("Beatra upload instructions are invalid") parsed = urllib.parse.urlsplit(url) if ( parsed.scheme != "https" or not parsed.hostname or parsed.username is not None or parsed.password is not None or parsed.fragment ): raise RuntimeError("Beatra upload instructions are invalid") if not all(isinstance(key, str) and isinstance(value, str) for key, value in headers.items()): raise RuntimeError("Beatra upload instructions are invalid") content_type = _header_value(headers, "Content-Type") content_length = _header_value(headers, "Content-Length") if content_type != mime_type or content_length != str(len(content)): raise RuntimeError("Beatra upload instructions are invalid") response = put_bytes(url, dict(headers), content) artifact_id = response.get("artifact_id") if not isinstance(artifact_id, str) or not artifact_id: raise RuntimeError("Beatra upload returned an invalid response") return {"type": "artifact", "artifact_id": artifact_id} ``` ### Technical Analysis The client validates the scheme, URL syntax, method, MIME type, and byte length, but accepts any ...[truncated 1452 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Allowlist exact upload domains or narrowly defined approved storage-domain suffixes. 2. Reject IP-literal destinations, local addresses, private-network addresses, and unapproved ports. 3. Require a signed upload grant that binds the destination hostname, object key, MIME type, byte length, expiry, and request identity. 4. Verify the grant using a pinned key independent of the response transport. 5. Display the destination domain before transmitting sensitive media where practical. 6. Preserve the existing regular-file, no-follow, size, MIME, and stability checks. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/mcp_client.py:1044
Finding
Windows Device Token confidentiality is assumed rather than enforced<![CDATA[ ## Vulnerability Details **File Location**: `scripts/authorize.py:112-123`; `scripts/mcp_client.py:1044-1051`; `references/installation-and-auth.md:16-21` **Vulnerability Type**: Missing Windows ACL creation and validation for a full-scope bearer credential **Risk Level**: Medium ### Vulnerable Code ```python def _private_directory(path: Path) -> None: path.mkdir(mode=0o700, parents=True, exist_ok=True) if os.name == "posix": path.chmod(0o700) def _restrict_file(path: Path) -> None: if os.name == "posix": path.chmod(0o600) ``` ```python def _read_private_credentials(state_dir: Path, path: Path) -> str: if os.name == "nt": # The state directory lives under the user profile, whose default # ACL is already private to the user (the gh/aws/gcloud posture). # The former custom DACL verification was dropped deliberately... return path.read_text(encoding="utf-8") ``` The documentation makes a stronger claim than the implementation guarantees: ```text On Windows the current user must be the only principal granted access through the file ACL. ``` ### Technical Analysis POSIX systems receive explicit owner-only directory and file modes, and credential reads verify ownership and permissions. On Windows, the implementation simply relies on inherited profile ACLs and reads the token without verifying that the current user is the only permitted principal. Inherited ACLs can be modified by administrators, enterprise policy, migration tools, shared-profile configurations, or prior directory changes. Because the token includes broad media and wallet-spending permissions, relying on an unverified default is not equivalent to enforcing the documented security requirement. ### Attack Path 1. `~/.beatra` is created in a Windows profile with an inherited ACL that permits another local principal or group to read files. 2. Authorization writes `credentials.json` without replacing or verifying the inher ...[truncated 510 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Store the token in Windows Credential Manager or another operating-system credential vault. 2. If a file must be used, create an explicit DACL granting access only to the current user and required system principals. 3. Verify the effective ACL before every credential read and fail closed if broader access exists. 4. Avoid relying solely on inherited profile-directory permissions. 5. Add Windows-specific tests for inherited groups, shared profiles, altered ACLs, and privilege transitions. 6. Reduce token scope so that exposure has a smaller impact. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/uninstall.py:226
Finding
Uninstall deletes the local credential after an unconfirmed revocation<![CDATA[ ## Vulnerability Details **File Location**: `scripts/uninstall.py:226-249` **Vulnerability Type**: Unsafe credential lifecycle handling during failed server revocation **Risk Level**: Medium ### Vulnerable Code ```python token = _device_token(state_dir) revoked = False revoke_state = "no_credential" if token is not None: try: status = post_revoke(token) if status == 200: revoked = True revoke_state = "revoked" elif status == 401: revoke_state = "not_recognized" else: result.update( { "decision": "revoke_retry", "revoked": False, "reason": f"http_{status}", } ) return result except RuntimeError: revoke_state = "unreachable" token = None _remove_local_state(state_dir) result.update({"decision": "disconnected", "revoked": revoked, "reason": revoke_state}) return result ``` ### Technical Analysis When the revocation endpoint returns a non-200/non-401 HTTP response, the script correctly retains local state for a retry. However, when the endpoint is unreachable and `post_revoke()` raises `RuntimeError`, the script records `unreachable` and proceeds to delete `credentials.json` and the associated local state. Deletion of a local bearer token is not equivalent to server-side revocation. If the token has previously been copied, remains in another process, or is present in an unintended backup, it can remain valid. Destroying the only local credential also prevents the script from retrying authenticated revocation later. The claimed 15-day idle expiry is only a fallback; use of a copied token may extend its sliding idle lifetime. ### Attack Path 1. An attacker or unintended local process obtains a copy of the Device Token. 2. The user initiates uninstall while the revocation service is unreachable. 3. `post_revoke()` raises `RuntimeError`. 4. The ...[truncated 672 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Preserve local revocation capability whenever server revocation cannot be confirmed. 2. Return a retry-required state for network failures, matching the behavior used for retryable HTTP failures. 3. If retaining the full credential is undesirable, store it temporarily in an encrypted OS credential vault until revocation succeeds. 4. Delete local state only after HTTP 200, HTTP 401, or verified user-performed Console revocation. 5. Clearly distinguish “local files removed” from “server authorization revoked” in all user-facing output. 6. Consider a server-supported revocation identifier that does not require retaining a broadly privileged access token. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (19)

Lp3

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding
The skill documents capabilities equivalent to shell execution, network access, local file access, and environment interaction, yet it declares no permissions or user-visible trust boundary. That mismatch is dangerous because users may invoke what appears to be a simple voice-generation skill without understanding it can access local resources, persist credentials, and perform remote operations.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The declared purpose is narrow—creating voice clips from a written list—but the documented behavior includes authentication flows, credential persistence, arbitrary remote MCP tool invocation, local file upload, telemetry/registration, uninstall logic, and self-update behavior. This description-behavior gap materially increases the risk of deceptive invocation and overbroad trust, because operators may approve a media task without realizing it can modify local state and communicate extensively with remote services.

Context-Inappropriate Capability

Medium
Confidence
90% confidence
Finding
The skill includes automatic package update, download, verification, replacement, rollback, and recovery behavior that is unrelated to the user-facing task of producing voice clips. Self-updating code paths expand the attack surface substantially: if the update channel, signing, or package ownership assumptions fail, the skill becomes a software installer/modifier rather than just a media tool.

Description-Behavior Mismatch

Medium
Confidence
89% confidence
Finding
The skill is presented as a voice-clip creation tool, but it also performs automatic remote update checks, downloads, and file replacement without separate confirmation. Even if the mechanism is described later in the file, the mismatch creates a social-engineering and consent problem because users may not expect package-owned files to change during normal task execution.

Context-Inappropriate Capability

Medium
Confidence
72% confidence
Finding
The file states that the bundled client makes a remote `beatra.installations.register` call on first use, transmitting package slug, version, platform, and a stable external installation reference. For a skill whose purpose is generating grid notice voice clips, this outbound registration is not functionally necessary and creates an undisclosed telemetry channel that can identify installations and environments, increasing privacy and tracking risk.

Description-Behavior Mismatch

High
Confidence
99% confidence
Finding
The OAuth scope string requests far more privilege than a skill for turning written notices into voice clips should need, including artifacts, images, videos, music, voice management, task control, and wallet spending. If this credential is compromised or the skill later misuses it, the token enables actions well beyond the stated purpose, violating least privilege and materially increasing blast radius.

Context-Inappropriate Capability

Critical
Confidence
97% confidence
Finding
Including voices:read, voices:write, tasks:read, tasks:cancel, and broad artifact permissions exceeds what is typically necessary for producing fixed voice clips from supplied text. These permissions could allow manipulation of voice assets or interference with user tasks, making the skill materially more dangerous than its stated function suggests.

Context-Inappropriate Capability

High
Confidence
97% confidence
Finding
Including voices:read, voices:write, tasks:read, tasks:cancel, and broad artifact permissions exceeds what is typically necessary for producing fixed voice clips from supplied text. These permissions could allow manipulation of voice assets or interference with user tasks, making the skill materially more dangerous than its stated function suggests.

Context-Inappropriate Capability

High
Confidence
97% confidence
Finding
Including voices:read, voices:write, tasks:read, tasks:cancel, and broad artifact permissions exceeds what is typically necessary for producing fixed voice clips from supplied text. These permissions could allow manipulation of voice assets or interference with user tasks, making the skill materially more dangerous than its stated function suggests.

Description-Behavior Mismatch

High
Confidence
92% confidence
Finding
The skill is presented as a voice-clip generation tool, but this client exposes generic remote MCP tool invocation, upload, registration, and update capabilities. That capability mismatch increases the risk of covert remote command brokering or abuse of broader platform actions than a user would reasonably expect from the advertised skill.

Context-Inappropriate Capability

Medium
Confidence
88% confidence
Finding
The code fingerprints the host environment and performs installation telemetry unrelated to the stated voice-pack purpose. Collecting platform identifiers and persistent installation references without necessity expands privacy exposure and can aid profiling or tracking across executions.

Context-Inappropriate Capability

High
Confidence
95% confidence
Finding
The package includes a remote self-update mechanism that downloads code and replaces local files, which is not justified by a simple voice-generation skill. Even with integrity checks, self-modifying code materially expands attack surface: compromise of the update channel, signing process, or distribution backend can convert this client into a persistence and code-delivery mechanism.

Missing User Warnings

Medium
Confidence
85% confidence
Finding
The file discloses automatic updates, but not prominently enough relative to the skill's innocuous description, despite stating that package-owned files may be replaced without separate confirmation. In context, that weak disclosure is risky because the skill already exercises network and local modification capabilities, making user consent and expectation-setting especially important.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The document states that the client silently checks for updates and installs them automatically by default before ordinary commands. Even though later text describes integrity checks and rollback protections, silent default replacement of installed software changes local files and execution behavior without explicit user confirmation at the time of change, which is a meaningful security and safety concern if users are unaware of it.

Missing User Warnings

Low
Confidence
80% confidence
Finding
The documentation says first use triggers a registration call and names several transmitted fields, but the skill context suggests users expect offline or task-focused voice generation rather than environment telemetry. Lack of a prominent warning and consent mechanism can mislead users about data transmission, creating a privacy and trust issue even if the data is not highly sensitive.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
Silent automatic updates modify installed files during normal operation without a user-facing prompt at execution time. This undermines user expectations and can introduce unreviewed code changes into a supposedly narrow-purpose skill, especially dangerous given the skill's broader-than-advertised remote capabilities.

Missing User Warnings

Low
Confidence
83% confidence
Finding
The client writes skill inventory metadata on every use without informing the user. While not directly a code-execution flaw, it creates undisclosed local tracking state and contributes to a broader pattern of covert telemetry inconsistent with the skill's stated purpose.

Missing User Warnings

Medium
Confidence
87% confidence
Finding
Best-effort installation registration sends package, version, platform, and installation reference data to a remote service without user-facing disclosure. In the context of a voice-pack skill, this is unnecessary telemetry that can support device/user correlation and hidden monitoring.

Self-Modification

High
Category
Rogue Agent
Content
)
    update = subparsers.add_parser(
        "update",
        help="Check, install, or configure Beatra package self-updates",
    )
    update.add_argument(
        "--check",
Confidence
96% confidence
Finding
Exposing self-update as a first-class CLI feature normalizes self-modification of the installed package. In a skill whose declared purpose is voice-clip generation, this creates an unnecessary pathway for local code replacement and persistence if the update ecosystem or remote service is ever compromised.

Static analysis

No suspicious patterns detected.