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

Content
View full analysis
"验证成功了,帮我把这三个 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
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
xenodia_client.py:39
Finding

Local EVM Private Key Is Written Without Enforced Owner-Only Permissions

Content
View full analysis
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
View remediation

T08 · Insecure Dependencies

Warning
Location
SKILL.md:69
Finding

Unpinned Third-Party Dependencies Are Installed Without Integrity Verification

Content
View full analysis
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
xenodia_client.py:48
Finding

Configurable Gateway Can Request Arbitrary Wallet Message Signatures

Content
View full analysis
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
View remediation
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
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · xenodia_cdp_client.py (reported line 111)May include surrounding context.

python
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
        )

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

Critical
Category
Data Flow
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.

Content

Scanner excerpt · xenodia_cdp_client.py (reported line 129)May include surrounding context.

python
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
        )

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · xenodia_cdp_client.py (reported line 142)May include surrounding context.

python
# ── 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
    )

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · xenodia_cdp_client.py (reported line 155)May include surrounding context.

python
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
    )

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · xenodia_cdp_client.py (reported line 178)May include surrounding context.

python
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}]},

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

Critical
Category
Data Flow
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.

Content

Scanner excerpt · xenodia_cdp_client.py (reported line 293)May include surrounding context.

python
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"])

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

Critical
Category
Data Flow
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.

Content

Scanner excerpt · xenodia_cdp_client.py (reported line 298)May include surrounding context.

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

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · xenodia_client.py (reported line 46)May include surrounding context.

python
def login(acc: Account) -> str:
    try:
        resp = requests.post(
            f"{XENODIA_BASE_URL}/v1/auth/challenge",
            json={"wallet_address": acc.address}, timeout=10
        )

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

Critical
Category
Data Flow
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.

Content

Scanner excerpt · xenodia_client.py (reported line 56)May include surrounding context.

python
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
        )

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · xenodia_client.py (reported line 73)May include surrounding context.

python
# ── 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)

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · xenodia_client.py (reported line 84)May include surrounding context.

python
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)

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · xenodia_client.py (reported line 114)May include surrounding context.

python
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}]},

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · xenodia_client.py (reported line 189)May include surrounding context.

python
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"])

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · xenodia_client.py (reported line 195)May include surrounding context.

python
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:

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.

Content

No source excerpt is available for this finding.

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.

Content

No source excerpt is available for this finding.

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · xenodia_cdp_client.py (reported line 109)May include surrounding context.

python
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",

Context-Inappropriate Capability

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

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

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

Content

No source excerpt is available for this finding.

Missing User Warnings

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

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

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.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
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.

Content

Scanner excerpt · SKILL.md (reported line 58)May include surrounding context.

md
> "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**:

Context-Inappropriate Capability

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

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

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

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

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

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.