Back to skill

Security audit

xenodia

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its Xenodia gateway purpose, but it handles wallet and API credentials in risky persistent ways that deserve review before installation.

Install only if you intentionally want an agent to authenticate with Xenodia using wallet credentials. Do not save CDP secrets in ~/.zshrc; use a dedicated secret store or tightly permissioned file, rotate any exposed keys, avoid custom XENODIA_BASE_URL values unless you control the endpoint, and review any proposed OpenClaw configuration change before applying it.

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

T09 · Insecure Skill Coding Practices

Error
Location
SKILL.md:112
Finding
Persistent Storage of CDP Credentials in a Plaintext Shell Startup File<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md`, lines 112-125 **Vulnerability Type**: Plaintext storage of long-lived credentials **Risk Level**: High ### Vulnerable Code ```markdown ### Step 4 — Persist credentials (do this once after verifying) Once the balance check passes, ask the agent to save the credentials to `~/.zshrc` so they're available in every future session without prompting: > "验证成功了,帮我把这三个 CDP 变量写到 ~/.zshrc 里保存起来。" The agent will append: ```bash export CDP_API_KEY_ID="..." export CDP_API_KEY_SECRET="..." export CDP_WALLET_SECRET="..." ``` ``` ### Technical Analysis The Skill explicitly instructs the agent to persist the CDP API key secret and wallet secret in `~/.zshrc`. Shell startup files are plaintext files intended for shell configuration, not secret storage. They may be exposed through backups, support bundles, accidental repository commits, shell-configuration synchronization, permissive file permissions, or local processes running under the same account. Persisting these credentials also increases their lifetime and exposure beyond the immediate Xenodia operation. The CDP credentials permit access to Coinbase CDP services, while the wallet secret is associated with managed-wallet operations. This is broader and more sensitive than the minimum privilege required for a one-time balance check or authentication attempt. ### Attack Path 1. The owner follows the Skill instruction and writes the three CDP values to `~/.zshrc`. 2. A local process, compromised utility, backup service, synchronization tool, or user with sufficient filesystem access reads the startup file. 3. The attacker extracts `CDP_API_KEY_ID`, `CDP_API_KEY_SECRET`, and `CDP_WALLET_SECRET`. 4. The attacker initializes a CDP client using the stolen credentials. 5. Subject to the permissions assigned to the CDP API key and wallet, the attacker performs unauthorized API or wallet-signing operations. ### Impact Assessment Successful exploitation can di ...[truncated 404 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Remove the instruction to append secrets to `~/.zshrc`. - Store credentials in an operating-system keychain, hardware-backed credential store, or dedicated secret manager. - Retrieve secrets only when required and keep them out of command histories, logs, prompts, and generated configuration files. - If file-based storage is unavoidable, use a dedicated file outside the project directory, create it atomically with mode `0600`, and verify ownership and permissions before loading it. - Use narrowly scoped CDP API credentials where supported. - Document credential rotation and revocation procedures. - Advise existing users to remove the values from shell startup files, inspect backups or synchronized dotfile repositories, and rotate all exposed credentials. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
xenodia_client.py:39
Finding
Local EVM Private Key Is Written Without Enforced Owner-Only Permissions<![CDATA[ ## Vulnerability Details **File Location**: `xenodia_client.py`, lines 39-43 **Vulnerability Type**: Insecure plaintext private-key storage **Risk Level**: High ### Vulnerable Code ```python def create_wallet() -> Account: acc = Account.create() with open(KEY_FILE, "w") as f: f.write(acc.key.hex()) return acc ``` ### Technical Analysis The wallet private key is written directly to `.xenodia_agent_key` as unencrypted hexadecimal text. The code uses ordinary `open()` and relies entirely on the process's current `umask`; it neither creates the file with an explicit owner-only mode nor verifies its ownership and permissions before later reads. On a system with a permissive `umask`, the resulting file may be readable by other local users. Even where permissions happen to be restrictive, plaintext storage leaves the key accessible to any process operating under the same user account, backup software, or accidental project-directory publication. An EVM private key is the root credential for the local wallet. Unlike a short-lived access token, disclosure cannot be mitigated merely by waiting for expiration. ### Attack Path 1. A user runs `python3 xenodia_client.py init` under a permissive filesystem `umask`, or the key file is later copied while losing restrictive permissions. 2. The script creates `.xenodia_agent_key` without explicitly applying mode `0600`. 3. Another local user, compromised process, backup service, or repository publication obtains the file. 4. The attacker imports the hexadecimal private key into another EVM-compatible client. 5. The attacker impersonates the wallet, signs authentication messages, and performs any operation authorized by that wallet identity. ### Impact Assessment The attacker obtains full control of the local EVM wallet identity represented by this key. This includes producing valid signatures, authenticating to Xenodia as the wallet, accessing services or balances bound to it, and potentiall ...[truncated 169 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Create the key file atomically with explicit owner-only permissions, for example with `os.open()` using `O_CREAT | O_EXCL | O_WRONLY` and mode `0o600`. - Reject symbolic links and pre-existing files to reduce overwrite and redirection risks. - Before every read, verify that the file is a regular file, is owned by the current user, and is not accessible by group or others. - Store the key in an operating-system keychain or encrypted keystore instead of plaintext where possible. - Keep the key outside the project or Skill directory to reduce accidental packaging or source-control exposure. - Add `.xenodia_agent_key` to ignore and packaging-exclusion rules. - Provide key rotation and wallet-migration guidance for any user who may have created the file with unsafe permissions. ]]>

T08 · Insecure Dependencies

Warning
Location
SKILL.md:69
Finding
Unpinned Third-Party Dependencies Are Installed Without Integrity Verification<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md`, line 69 **Vulnerability Type**: Unsafe dependency installation **Risk Level**: Medium ### Vulnerable Code ```bash pip install cdp-sdk requests ``` ### Technical Analysis The installation instruction retrieves the latest available versions of `cdp-sdk` and `requests` from the user's configured Python package index. No reviewed version constraints, lock file, package hashes, or trusted-index requirements are provided. Consequently, the effective code executed by the Skill can change after this audit. A compromised package release, compromised package index, maliciously configured mirror, or incompatible future version could execute code during installation or import. Because the CDP client receives highly sensitive API and wallet credentials, dependency compromise has particularly significant consequences. The audit found no evidence that the named packages are themselves malicious; the vulnerability is the absence of reproducible and integrity-verified dependency controls. ### Attack Path 1. An attacker compromises a relevant package release, the configured package index, or a package mirror. 2. The user follows the documented `pip install` instruction. 3. `pip` downloads and installs the attacker-controlled distribution without validating a project-supplied hash. 4. Malicious installation or runtime code executes with the user's privileges. 5. The malicious dependency reads environment variables or wallet files and exfiltrates credentials, modifies local data, or takes other actions available to the user. ### Impact Assessment Exploitation can provide arbitrary code execution with the privileges of the user running `pip` or the client. Accessible data may include CDP API credentials, the CDP wallet secret, the local EVM private key, Xenodia bearer tokens, generated API keys, prompts, and user-owned files. If installation is performed in a privileged environment, the impact may extend to ...[truncated 43 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Pin every direct and transitive dependency to reviewed versions. - Publish a lock file generated from a controlled build environment. - Include cryptographic hashes and install with `pip --require-hashes`. - Use the official Python package index or an explicitly trusted internal mirror. - Install into an isolated virtual environment under a non-privileged account. - Regularly scan pinned dependencies for known vulnerabilities and review updates before changing versions. - Consider distributing a signed package or reproducible environment definition rather than instructing users to install unconstrained latest versions. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
xenodia_client.py:48
Finding
Configurable Gateway Can Request Arbitrary Wallet Message Signatures<![CDATA[ ## Vulnerability Details **File Location**: `xenodia_client.py`, lines 19 and 48-65; `xenodia_cdp_client.py`, lines 56 and 116-142 **Vulnerability Type**: Insufficient validation of wallet-signing requests **Risk Level**: High ### Vulnerable Code Local-wallet implementation: ```python XENODIA_BASE_URL = os.environ.get("XENODIA_BASE_URL", "https://api.xenodia.xyz") ``` ```python def login(acc: Account) -> str: try: resp = requests.post( f"{XENODIA_BASE_URL}/v1/auth/challenge", json={"wallet_address": acc.address}, timeout=10 ) resp.raise_for_status() data = resp.json() signable_message = encode_defunct(text=data["message"]) signature = acc.sign_message(signable_message).signature.hex() resp = requests.post( f"{XENODIA_BASE_URL}/v1/auth/verify", json={"challenge_id": data["challenge_id"], "signature": signature}, timeout=10 ) resp.raise_for_status() return resp.json()["tokens"] ``` CDP-wallet implementation: ```python XENODIA_BASE_URL = os.environ.get("XENODIA_BASE_URL", "https://api.xenodia.xyz") ``` ```python async def _sign_message(cdp, account, message: str) -> str: """Sign an EIP-191 personal message via CDP MPC (no local private key). Uses cdp.evm.sign_message(address, message) — accepts raw string, returns '0x...' hex signature directly. """ try: sig = await cdp.evm.sign_message(address=account.address, message=message) return sig except Exception as e: print(f"[!] CDP signing error: {e}", file=sys.stderr) sys.exit(1) async def _login(cdp, account) -> str: """Authenticate with Xenodia and return JWT access token.""" try: resp = requests.post( f"{XENODIA_BASE_URL}/v1/auth/challenge", json={"wallet_address": account.address}, timeout=10 ) resp.raise_for_status() data = resp.json() ...[truncated 2799 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Allowlist the expected Xenodia hostname by default and require an explicit, prominent opt-in for custom gateway hosts. - Require HTTPS and reject URLs containing unexpected schemes, credentials, fragments, or unapproved ports. - Define a strict, domain-separated authentication challenge format. - Before signing, validate the expected service domain, wallet address, operation purpose, chain or application identifier where applicable, nonce, issue time, and short expiration time. - Reject unknown fields, stale challenges, malformed challenges, or messages that do not exactly match the authentication schema. - Prefer a standardized structured-signing scheme where supported, with explicit domain separation. - Display or log a safe summary of the signing purpose without exposing credentials. - Keep server-side challenges single-use and bind them to the wallet, requesting client, intended service, and a short validity period. ]]>
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
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
Findings (43)

Tainted flow: 'XENODIA_BASE_URL' from os.environ.get (line 53, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Content
async def _login(cdp, account) -> str:
    """Authenticate with Xenodia and return JWT access token."""
    try:
        resp = requests.post(
            f"{XENODIA_BASE_URL}/v1/auth/challenge",
            json={"wallet_address": account.address}, timeout=10
        )
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'XENODIA_BASE_URL' from os.environ.get (line 53, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Content
signature = await _sign_message(cdp, account, message)

    try:
        resp = requests.post(
            f"{XENODIA_BASE_URL}/v1/auth/verify",
            json={"challenge_id": challenge_id, "signature": signature}, timeout=10
        )
Confidence
90% confidence
Finding
This request sends a wallet signature to whatever host is configured in XENODIA_BASE_URL. If an attacker can influence that environment variable, they can obtain signed authentication material and potentially mint or misuse session tokens against their own service or deceive operators into authenticating to an attacker-controlled endpoint.

Tainted flow: 'XENODIA_BASE_URL' from os.environ.get (line 53, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
# ── Sync API calls (no CDP needed after login) ────────────────────────────────

def _get_balance(token: str):
    resp = requests.get(
        f"{XENODIA_BASE_URL}/v1/credits/balance",
        headers={"Authorization": f"Bearer {token}"}, timeout=10
    )
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'XENODIA_BASE_URL' from os.environ.get (line 53, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
print(f"Balance: {micro / 1_000_000:.6f} USDC ({micro} micro-USDC)")

def _list_models(token: str) -> list:
    resp = requests.get(
        f"{XENODIA_BASE_URL}/v1/models",
        headers={"Authorization": f"Bearer {token}"}, timeout=10
    )
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'XENODIA_BASE_URL' from os.environ.get (line 53, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Content
return resp.json().get("data", [])

def _chat(token: str, model: str, prompt: str):
    resp = requests.post(
        f"{XENODIA_BASE_URL}/v1/chat/completions",
        headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
        json={"model": model, "messages": [{"role": "user", "content": prompt}]},
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'XENODIA_BASE_URL' from os.environ.get (line 53, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
async with make_cdp_client() as cdp:
                account = await _get_or_create_account(cdp)
                access_token = (await _login(cdp, account))["access_token"]
                resp = requests.get(f"{XENODIA_BASE_URL}/v1/me/api-keys", headers={"Authorization": f"Bearer {access_token}"})
                data = resp.json().get("data")
                if data and data.get("token"):
                    print(data["token"])
Confidence
96% confidence
Finding
This endpoint retrieves account API keys using a bearer access token and sends that token to a host derived from XENODIA_BASE_URL. If the base URL is attacker-controlled, the code can disclose session credentials and facilitate unauthorized key-management actions against an unintended service.

Tainted flow: 'XENODIA_BASE_URL' from os.environ.get (line 53, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Content
if data and data.get("token"):
                    print(data["token"])
                else:
                    resp = requests.post(f"{XENODIA_BASE_URL}/v1/me/api-keys", headers={"Authorization": f"Bearer {access_token}"})
                    print(resp.json()["data"]["token"])
        run_async(_run())
Confidence
97% confidence
Finding
This code can create an API key by POSTing authenticated requests to a host controlled by XENODIA_BASE_URL. In combination with the broader get-api-key behavior, a malicious endpoint override could capture bearer tokens and induce creation or disclosure of long-lived credentials.

Tainted flow: 'XENODIA_BASE_URL' from os.environ.get (line 20, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Content
def login(acc: Account) -> str:
    try:
        resp = requests.post(
            f"{XENODIA_BASE_URL}/v1/auth/challenge",
            json={"wallet_address": acc.address}, timeout=10
        )
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'XENODIA_BASE_URL' from os.environ.get (line 20, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Content
signable_message = encode_defunct(text=data["message"])
        signature = acc.sign_message(signable_message).signature.hex()

        resp = requests.post(
            f"{XENODIA_BASE_URL}/v1/auth/verify",
            json={"challenge_id": data["challenge_id"], "signature": signature}, timeout=10
        )
Confidence
90% confidence
Finding
The client signs an arbitrary server-supplied message from the challenge response without validating its content, origin constraints, or expected format. If XENODIA_BASE_URL is redirected to a malicious server or the upstream service is compromised, the code can produce reusable wallet signatures over attacker-chosen content, which may enable wallet-auth abuse or unintended authorization in other systems.

Tainted flow: 'XENODIA_BASE_URL' from os.environ.get (line 20, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
# ── API ───────────────────────────────────────────────────────────────────────

def get_balance(token: str):
    resp = requests.get(f"{XENODIA_BASE_URL}/v1/credits/balance",
                        headers={"Authorization": f"Bearer {token}"}, timeout=10)
    if resp.status_code == 401:
        print("[!] Wallet is not bound to any Xenodia owner account.", file=sys.stderr)
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'XENODIA_BASE_URL' from os.environ.get (line 20, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
print(f"Balance: {micro / 1_000_000:.6f} USDC ({micro} micro-USDC)")

def list_models(token: str) -> list:
    resp = requests.get(f"{XENODIA_BASE_URL}/v1/models",
                        headers={"Authorization": f"Bearer {token}"}, timeout=10)
    if resp.status_code == 402:
        print("[!] 402 Payment Required: Wallet balance is below minimum threshold ($5).", file=sys.stderr)
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'XENODIA_BASE_URL' from os.environ.get (line 20, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Content
print(f"Available: {', '.join(ids)}")

def chat(token: str, model: str, prompt: str):
    resp = requests.post(
        f"{XENODIA_BASE_URL}/v1/chat/completions",
        headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
        json={"model": model, "messages": [{"role": "user", "content": prompt}]},
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'XENODIA_BASE_URL' from os.environ.get (line 20, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
access_token = login(acc)["access_token"]
        
        # Try to get existing key
        resp = requests.get(f"{XENODIA_BASE_URL}/v1/me/api-keys", headers={"Authorization": f"Bearer {access_token}"})
        data = resp.json().get("data")
        if data and data.get("token"):
            print(data["token"])
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'XENODIA_BASE_URL' from os.environ.get (line 20, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Content
print(data["token"])
        else:
            # Generate a new one
            resp = requests.post(f"{XENODIA_BASE_URL}/v1/me/api-keys", headers={"Authorization": f"Bearer {access_token}"})
            print(resp.json()["data"]["token"])
    elif cmd == "chat":
        if len(args) < 3:
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
This mismatch is especially concerning because the skill not only authenticates but can retrieve or create long-term Xenodia API keys and print raw credentials to stdout. Exposing or minting credentials is materially more sensitive than simple balance/model checks, so incomplete disclosure can lead to unjustified trust and overbroad use.

Tp4

High
Category
MCP Tool Poisoning
Confidence
92% confidence
Finding
This mismatch is especially concerning because the skill not only authenticates but can retrieve or create long-term Xenodia API keys and print raw credentials to stdout. Exposing or minting credentials is materially more sensitive than simple balance/model checks, so incomplete disclosure can lead to unjustified trust and overbroad use.

Credential Access

High
Category
Privilege Escalation
Content
sys.exit(1)

async def _login(cdp, account) -> str:
    """Authenticate with Xenodia and return JWT access token."""
    try:
        resp = requests.post(
            f"{XENODIA_BASE_URL}/v1/auth/challenge",
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Context-Inappropriate Capability

High
Confidence
99% confidence
Finding
Creating persistent Xenodia API keys is materially more sensitive than transient wallet-based login and exceeds the minimum privileges needed for balance/model/chat operations. In an agent-skill context, this is dangerous because it can silently convert a scoped session into a reusable credential that may outlive the current run and be reused elsewhere.

Description-Behavior Mismatch

High
Confidence
96% confidence
Finding
The implemented get-api-key command adds credential retrieval/generation capability that is not disclosed in the stated skill scope or top-level usage text. Hidden or undocumented secret-management features are dangerous because they expand the skill's ability to mint and exfiltrate long-lived credentials beyond what reviewers and operators expect.

Missing User Warnings

High
Confidence
99% confidence
Finding
The get-api-key command retrieves or creates an API key and prints it directly to stdout without confirmation or warnings. API keys are typically longer-lived than access tokens, so leakage via terminal history, logs, agent transcripts, or pipeline output can enable durable unauthorized access.

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill clearly instructs use of environment variables, local file reads/writes, and outbound network access, yet declares no explicit tool scope or permission boundaries. That omission increases the chance an agent will apply broader capabilities than the owner expects, especially because the skill also handles authentication material and configuration changes.

Session Persistence

Medium
Category
Rogue Agent
Content
> "I need you to get 3 things from portal.cdp.coinbase.com:
>
> 1. **CDP_API_KEY_ID** and **CDP_API_KEY_SECRET**:
>    Go to portal → top-left menu → **API Keys** → **Create API Key**
>    → After creation, copy the `"id"` field (= CDP_API_KEY_ID)
>    and the `"privateKey"` field (= CDP_API_KEY_SECRET, a base64 string ~88 chars)
> 2. **CDP_WALLET_SECRET**:
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Context-Inappropriate Capability

Medium
Confidence
97% confidence
Finding
The skill instructs the agent to append highly sensitive CDP secrets to ~/.zshrc, creating long-lived local persistence of credentials in a broadly sourced shell startup file. This increases exposure to accidental disclosure, shell history or backup leakage, overbroad access by other processes, and reuse outside the intended Xenodia workflow.

Natural-Language Policy Violations

Medium
Confidence
97% confidence
Finding
The markdown instructs the user to say a specific Chinese sentence to persist credentials, which effectively imposes a language-specific interaction pattern. The file does not offer an English alternative at that point or state that Chinese is optional, so this can violate a language/locale policy requiring user choice.

Context-Inappropriate Capability

Medium
Confidence
94% confidence
Finding
The skill directs the agent to modify local LLM configuration files, which extends beyond simple gateway use into persistent system reconfiguration. That can alter future model routing, cause unintended vendor lock-in or service disruption, and may embed newly generated long-term credentials into local config without adequate approval boundaries.

Static analysis

No suspicious patterns detected.