Back to skill

Security audit

hanzi-card-set

Security checks for vulnerabilities and agentic risk

Overview

The skill does make hanzi image cards, but it also asks for broad Beatra account permissions, stores a shared bearer token, supports unrestricted remote tool calls, and silently self-updates package files by default.

Review this skill carefully before installing. It is not just a local flashcard helper: it connects to Beatra, stores a reusable shared device token, may spend credits for generation, can call broad remote MCP tools, uploads user-selected files, sends installation metadata, and silently updates itself unless disabled with the documented update setting. Prefer installation only in an environment where those account permissions, telemetry, and automatic updates are acceptable.

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

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/authorize.py:32
Finding
Overprivileged Shared Device Token and Unrestricted MCP Tool Dispatch<![CDATA[ ## Vulnerability Details **File Location**: `scripts/authorize.py:32-36`, `scripts/mcp_client.py:1470-1478` **Vulnerability Type**: Excessive authorization scope and unrestricted privileged tool selection **Risk Level**: High ### Complete Code Snippet ```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 call = subparsers.add_parser("call", help="Call one tool with a JSON object on stdin") call.add_argument("tool_name") ``` ```python assert tool_name is not None return session.request( 2, "tools/call", {"name": tool_name, "arguments": arguments}, ) ``` ### Technical Analysis The Skill's declared purpose is to create Hanzi recognition-card images. Its authorization scope nevertheless grants unrelated video, music, speech, voice-write, task-cancellation, artifact-write, and general wallet-spending privileges. The bundled client also accepts an arbitrary MCP tool name and forwards it to the service without enforcing a package-specific allowlist. Consequently, the extra privileges are operationally accessible rather than merely present in an unused token. This violates least privilege. A shared bearer credential with broad spending and content-generation authority materially expands the consequences of malicious instructions, local compromise, or unintended invocation. The documentation confirms that one approval covers multiple media types, but disclosure does not make those unrelated permissions necessary for this Skill's stated functionality. ### Attack Path 1. The user authorizes the Skill through `scripts/authorize.py`. 2. Beatra issues a bearer token containing the complete broad scope. 3. The token is stored in `~/.beatra/credentials.json`. 4. An attacker who can influence Agent commands, modify local Skill instructions, or invoke the client calls: `python3 scripts/m ...[truncated 839 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Issue a package-specific token with only the scopes needed for Hanzi card generation: - Image generation. - Model listing. - Necessary artifact upload/read access. - Task status reads. - Narrow billing authority limited to approved image-generation operations. 2. Remove video, music, speech, voice-write, and task-cancellation scopes unless a documented feature requires them. 3. Replace unrestricted `call.add_argument("tool_name")` forwarding with a strict allowlist, such as: - `beatra.models.list` - `beatra.images.generate` - `beatra.images.edit` - `beatra.assets.upload` - Required task and wallet read operations 4. Reject unknown or unrelated tool names locally before establishing a privileged session. 5. Separate read-only and spending capabilities into different credentials or require explicit elevation immediately before a billable call. 6. Enforce equivalent tool restrictions on the server so a modified local client cannot bypass the package policy. ]]>

other

Warning
Location
scripts/authorize.py:363
Finding
Unnecessary Hostname and Persistent Installation Telemetry Collection<![CDATA[ ## Vulnerability Details **File Location**: `scripts/authorize.py:363-371`, `scripts/authorize.py:466-479`, `scripts/mcp_client.py:1354-1384` **Vulnerability Type**: Collection and transmission of host-identifying metadata beyond core functionality **Risk Level**: Medium ### Complete Code Snippet ```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) ``` ```python result = session.request( 2, "tools/call", { "name": "beatra.installations.register", "arguments": { "package_slug": PACKAGE_SLUG, "package_version": PACKAGE_VERSION, "platform": host_platform(state_dir), "external_installation_ref": _registration_reference(state_dir), }, }, timeout=REGISTRATION_TIMEOUT_SECONDS, ) ``` ### Technical Analysis The authorization helper reads the local hostname through `socket.gethostname()`. It sends that value to `https://api.beatra.ai` as `device_name`, together with the Agent platform, package identity, package version, and a stable installation reference. The MCP client separately performs best-effort installation registration and transmits the package metadata, environment-derived platform, and persistent installation identifier. Registration freshness is cached for 24 hours, after which telemetry ma ...[truncated 1550 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Do not collect or transmit the hostname by default. 2. Use a generic device label or ask the user to provide an optional display name. 3. Present an explicit disclosure before authorization listing every transmitted metadata field and its purpose. 4. Make installation telemetry opt-in and allow it to be disabled independently of core MCP functionality. 5. Replace persistent installation correlation identifiers with short-lived or privacy-preserving identifiers where possible. 6. Minimize registration frequency and avoid transmitting fields already available from authenticated protocol context. 7. Document retention, access, and deletion controls for device and installation telemetry. 8. Keep platform detection local unless it is strictly necessary for protocol compatibility. ]]>

T03 · Remote Payload Retrieval and Execution

Error
Location
scripts/mcp_client.py:969
Finding
Silent Remote Replacement of Executable Skill Files Without Pinned Signature Verification<![CDATA[ ## Vulnerability Details **File Location**: `scripts/mcp_client.py:969-1013`, `scripts/mcp_client.py:1543-1544` **Vulnerability Type**: Automatic remote payload retrieval and later execution **Risk Level**: High ### Complete Code Snippet ```python def maybe_auto_update( *, state_dir: Path | None = None, install_root: Path | None = None, get_bytes: GetBytes = _default_get_bytes, now: float | None = None, ) -> bool: """Best-effort silent update. Never block the requested MCP command.""" resolved_state = state_dir or Path.home() / ".beatra" try: resolved_root = (install_root or _current_install_root()).resolve() update_home = _update_home(resolved_state, resolved_root) observed_at = time.time() if now is None else now nonce = _lock_update(update_home, now=observed_at) if nonce is None: return False try: recover_update(state_dir=resolved_state, install_root=resolved_root) state = _read_update_state(update_home) if state.get("auto_update", True) is False: return False last_checked = state.get("last_checked_at") if ( isinstance(last_checked, (int, float)) and observed_at - float(last_checked) < UPDATE_CHECK_MAX_AGE_SECONDS ): return False state["last_checked_at"] = observed_at _write_private_json(update_home / "state.json", state) 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_roo ...[truncated 3417 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Disable automatic installation by default. Automatic checks may remain optional, but installation should require explicit informed approval. 2. Sign release manifests using an offline-protected package-signing key. 3. Pin the corresponding verification public key in the installed client and reject unsigned or incorrectly signed releases. 4. Use a transparent release log or reproducible package registry so users can independently verify published versions. 5. Show the target version, changed files, publisher identity, and signature status before replacement. 6. Separate update checking, downloading, and installation into distinct commands. 7. Consider pinning the installed version until the user or package manager explicitly upgrades it. 8. Preserve the existing archive traversal, checksum, ownership, rollback, and size-limit protections as defense in depth. 9. Ensure update state cannot be silently reset to the default-enabled condition after corruption or deletion. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/mcp_client.py:1044
Finding
Windows Bearer Credential ACL Requirement Is Not Enforced<![CDATA[ ## Vulnerability Details **File Location**: `scripts/authorize.py:120-130`, `scripts/mcp_client.py:1044-1051` **Vulnerability Type**: Insecure local credential storage and permission validation on Windows **Risk Level**: Medium ### Complete Code Snippet ```python def _private_directory(path: Path) -> None: # POSIX gets explicit 700/600. On Windows the state directory lives under # the user profile, whose default ACL is already private to the user — # the same posture as gh/aws/gcloud credential stores. The former custom # DACL ceremony was dropped deliberately: its command patterns read as # hostile to agent safety policies and endpoint security, failing installs # while adding no protection an elevated administrator could not bypass. path.mkdir(mode=0o700, parents=True, exist_ok=True) if os.name == "posix": path.chmod(0o700) ``` ```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: its # command patterns read as hostile to agent safety policies and # endpoint security, failing installs while adding nothing an # elevated administrator could not bypass. return path.read_text(encoding="utf-8") ``` The documentation states: ```text On Windows the current user must be the only principal granted access through the file ACL. ``` ### Technical Analysis On POSIX, the client verifies ownership and exact `0700`/`0600` modes and uses no-follow file access. On Windows, it assumes that the user-profile ACL is sufficiently private and reads the plaintext bearer token without creating or validating a user-only discretionary access control list. The `mode=0o700` argument to `Path.mkdir()` does not establish the documented Windo ...[truncated 1555 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Store the bearer credential in Windows Credential Manager or protect it with DPAPI bound to the current user. 2. If file storage remains necessary, create an explicit ACL granting access only to the current user and required system principals. 3. Validate the effective ACL before every credential read and reject credentials stored in an unsafe location. 4. Prevent inheritance from introducing additional read principals, or verify inherited entries against a strict policy. 5. Use safe Windows APIs through a reviewed library rather than shelling out to permission-management utilities. 6. Document the exact acceptable principals and account for managed or domain-joined systems. 7. Reduce token scope so local disclosure has limited consequences. 8. Provide a secure migration path that moves existing plaintext credentials into protected storage and revokes superseded tokens. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (25)

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill exposes broad code-capable behaviors (environment access, file read/write, network, and shell) without explicitly declaring or constraining those permissions. That creates a transparency and containment gap: users and hosting systems may treat it as a simple card-generation skill while it can perform sensitive local and remote operations, increasing the risk of credential access, filesystem modification, or arbitrary command execution if misused or compromised.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The documented behavior substantially exceeds the stated purpose: beyond generating hanzi cards, it performs authentication flows, stores persistent credentials, communicates with remote services using bearer tokens, can upload files, self-update, register installations, and revoke/delete local state. This mismatch is dangerous because it defeats informed consent and trust boundaries; a user invoking a seemingly narrow content-creation skill may unknowingly authorize credential handling, data exfiltration, and software modification.

Context-Inappropriate Capability

Medium
Confidence
97% confidence
Finding
The skill includes automatic self-update logic that downloads and replaces package files, which is unrelated to the core task of generating card sets. Even with claimed verification, any auto-update mechanism expands the attack surface significantly: compromise of the update channel, signing process, or discovery service could lead to arbitrary code delivery under the guise of a benign skill.

Description-Behavior Mismatch

Medium
Confidence
94% confidence
Finding
The documentation states that one approval covers image, video, music, speech, upload, model, and task tools, which is far broader than a skill described as a simple hanzi card generator. This creates a capability/scope mismatch that can enable over-privileged remote access and surprise tool use unrelated to the stated purpose.

Context-Inappropriate Capability

Medium
Confidence
93% confidence
Finding
The skill instructs the agent to establish and persist a long-lived remote credential in user storage, despite the manifest describing a narrow local-seeming card-generation function. Persistent device-token handling materially increases the blast radius if the skill, host, or filesystem is compromised, and it is not justified by the advertised functionality.

Context-Inappropriate Capability

High
Confidence
97% confidence
Finding
The bundled MCP client supports remote tool discovery and arbitrary tool invocation (`tools/list`, `tools/call`) against a remote endpoint, which exceeds the narrow stated purpose of producing hanzi cards. This effectively grants a generic remote capability channel that could be repurposed for unrelated actions, data access, or future-expanded server-side behavior without corresponding user expectation.

Description-Behavior Mismatch

High
Confidence
99% confidence
Finding
The authorization helper requests a very broad OAuth scope set, including artifacts, image/video/music/speech generation, voice management, wallet spending, and task control, which is far beyond what a hanzi flashcard generator appears to need. Over-scoped credentials violate least privilege and materially increase blast radius if the token is misused, stolen, or if the skill behaves unexpectedly.

Context-Inappropriate Capability

Critical
Confidence
100% confidence
Finding
Requesting wallet spending plus multiple unrelated media-generation capabilities is especially dangerous because it grants the skill authority to incur cost and access privileged platform actions unrelated to generating still hanzi cards. In this skill context, those permissions are unjustified, so compromise or abuse could lead to unauthorized spending and broad cross-service actions under the user's account.

Description-Behavior Mismatch

High
Confidence
94% confidence
Finding
The file implements a generic remote MCP client with self-update, package registration, telemetry, upload, and arbitrary tool-dispatch behavior that materially exceeds the declared purpose of a hanzi flashcard generator. In this skill context, the capability mismatch is dangerous because users and reviewers may grant trust expecting local card-generation logic while the package can instead act as a general remote command conduit and mutable downloader.

Context-Inappropriate Capability

Medium
Confidence
87% confidence
Finding
The code fingerprints the host agent/platform using environment variables and local host state, then transmits platform attribution with tool calls and registration. For a hanzi card skill, this collection is not clearly necessary, so it increases privacy and tracking risk and broadens the blast radius of any backend misuse.

Context-Inappropriate Capability

High
Confidence
95% confidence
Finding
The upload path reads arbitrary local files and uploads them to a remote service after obtaining server instructions, which is far broader than turning character lists into flashcards. In this skill context, that creates a meaningful exfiltration surface because any workflow invoking this helper could transfer sensitive local content under the cover of an unrelated creative feature.

Context-Inappropriate Capability

High
Confidence
96% confidence
Finding
The code can fetch remote manifests and archives, validate them, and overwrite the installed package, giving the skill a durable self-modifying capability. Even with integrity checks, this is a powerful supply-chain and persistence mechanism that is unjustified for a simple hanzi card generator and increases risk if the update channel, signing process, or backend is compromised.

Context-Inappropriate Capability

Medium
Confidence
88% confidence
Finding
The code records local skill inventory and sends installation registration telemetry unrelated to the declared flashcard-generation task. In this context, that creates unnecessary privacy exposure and can support cross-installation tracking without a clear feature justification.

Description-Behavior Mismatch

High
Confidence
95% confidence
Finding
The uninstall script is operating on shared Beatra device authorization state and contacting a remote revocation endpoint, which is outside the declared hanzi card generation purpose of the skill. Even if intended as lifecycle management, this gives the package authority over shared credentials used by other skills, creating a cross-skill trust and availability risk if invoked unexpectedly or by a compromised agent.

Context-Inappropriate Capability

High
Confidence
97% confidence
Finding
This code performs a privileged remote action by sending the device bearer token to a revocation API. In the context of a flashcard skill, embedding credential revocation logic is unnecessary and dangerous because execution of the script can disable shared platform access and affect unrelated installed skills if inventory logic is wrong, tampered with, or incomplete.

Context-Inappropriate Capability

High
Confidence
96% confidence
Finding
The script reads the shared access token from credentials.json and deletes shared local state files under ~/.beatra. Because these files are not scoped to this skill alone, a skill package gains the ability to inspect and destroy authentication and installation state for the broader environment, which exceeds its stated function and creates both confidentiality and availability risks.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
The documentation states that the client silently checks for and automatically installs newer releases by default without separate confirmation. Even though the text describes integrity checks and rollback protections, silent default modification of local package files increases supply-chain and user-consent risk because users may not realize software on their system changes automatically.

Missing User Warnings

Medium
Confidence
87% confidence
Finding
The documentation describes an automatic installation-registration call that transmits package and environment metadata and writes a local registration cache, but it does not mention any explicit user notice, consent, or opt-out. Even if the data is described as non-secret and non-billable, this is still telemetry-like behavior, and silent collection/transmission of environment identifiers can violate user expectations, privacy requirements, or enterprise policy.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
maybe_auto_update() silently performs network checks and can replace local package files during normal execution without a contemporaneous user-facing warning. In a skill whose stated purpose is card generation, this hidden behavior undermines user expectations and can enable unnoticed code changes or persistence through routine use.

Ssd 3

Medium
Confidence
84% confidence
Finding
On Windows, the credential reader explicitly skips validating file privacy/ACL posture and simply reads the credentials file, with comments normalizing the relaxed protection. That weakens defense in depth for bearer tokens because a mispermissioned profile or shared environment could expose reusable credentials without client-side detection.

Credential Access

High
Category
Privilege Escalation
Content
- `~/.beatra/installation.json` contains one stable, non-secret installation
  reference.
- `~/.beatra/credentials.json` contains the single Device Token.

On POSIX systems the directory must be mode `0700` and both files mode `0600`.
On Windows the current user must be the only principal granted access through
Confidence
92% confidence
Finding
The documentation establishes storage and use of a persistent device token in a local credentials file, giving the skill access to a reusable authentication secret. Even though it advises restrictive permissions, introducing credential acquisition and storage into a narrowly described skill materially increases exposure to credential theft, misuse, or unauthorized reuse.

Credential Access

High
Category
Privilege Escalation
Content
4. polls every 5 seconds for up to 15 minutes while the user signs in (or
   creates their account) and selects Allow;
5. atomically saves the returned Device Token to
   `~/.beatra/credentials.json` without printing an HTTP response body;
6. validates the new credential with the same non-billable MCP request and
   prints Ready only after it succeeds.
Confidence
91% confidence
Finding
The helper saves a returned device token to disk for future reuse, which creates an enduring credential target on the local system. For a skill advertised as a hanzi card generator, this persistent secret handling is disproportionate and increases the consequences of compromise beyond the expected feature set.

Credential Access

High
Category
Privilege Escalation
Content
#: these and then removes the directory only if it is empty — the script
#: never recursively deletes a directory it does not fully understand.
_STATE_FILES = (
    "credentials.json",
    "installation.json",
    "host.json",
    "skills.json",
Confidence
90% confidence
Finding
Referencing credentials.json as part of the state the script may remove indicates the skill is aware of and can affect shared credential material. In this skill context, access to credential-bearing files is unrelated to card generation and expands the package’s privileges into sensitive authentication management.

Credential Access

High
Category
Privilege Escalation
Content
def _device_token(state_dir: Path) -> str | None:
    path = state_dir / "credentials.json"
    try:
        value = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, ValueError):
Confidence
98% confidence
Finding
The _device_token function explicitly opens credentials.json and extracts an access_token, giving the skill direct read access to bearer credentials. This is dangerous because any code path, future modification, or compromise of the skill could exfiltrate or misuse the token, and the skill’s advertised flashcard purpose provides no legitimate reason to access it.

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
95% confidence
Finding
The package exposes a self-update command and supporting logic that can overwrite its own installed files, giving it self-modification capability. In the context of a narrowly described hanzi card skill, this is an unjustified persistence mechanism and materially raises the consequences of backend, supply-chain, or policy failures.

Static analysis

No suspicious patterns detected.