Back to skill

Security audit

Clawd Casino

Security checks for vulnerabilities and agentic risk

Overview

This real-money casino skill is coherent in purpose, but it needs careful review because it handles wallet private keys, API keys, and large USDC spending approvals with broad trust in remote/API-provided data.

Install only if you are comfortable trusting Clawd Casino's API and any configured CASINO_API_URL with real-money betting operations. Use a fresh wallet funded only with the amount you are willing to risk, avoid the default 1,000,000 USDC approval where possible, verify the API origin before use, do not pass a private key to MCP tooling, and treat any printed or saved private/API key as sensitive loggable material.

Vulnerability Patterns
  • 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
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (5)

other

Error
Location
SKILL.md:753
Finding

MCP Registration Workflow Requires Disclosure of the Wallet Private Key

Content
View full analysis
Remediation
View remediation

T05 · Unauthorized Access and Privilege Escalation

Error
Location
script/approve.py:25
Finding

Server-Controlled Spenders Receive Excessive USDC Permit Allowances

Content
View full analysis
list: """Get list of all games that can be approved.""" response = requests.get(f"{API_URL}/approve/game", headers=get_api_key_header()) if response.status_code != 200: # Fallback to known games if endpoint doesn't exist yet return [ {"name": "pvp", "display_name": "PvP Betting"}, {"name": "roulette", "display_name": "Roulette"}, ] result = response.json() if result.get("success"): return result.get("game", []) return [] def approve_game(game_name: str, amount: Optional[float] = None) -> bool: wallet = get_wallet_address() if not wallet: print("Error: CASINO_WALLET_KEY not set") return False response = requests.get( f"{API_URL}/approve/{game_name}/permit-nonce", headers=get_api_key_header() ) if response.status_code != 200: print(f"Error: Could not get permit nonce for {game_name}") return False nonce_data = response.json() nonce = nonce_data.get("nonce", 0) spender = nonce_data.get("spender") if not spender: print(f"Error: Game address not available for {game_name}") return False if amount: value = int(amount * 10**6) else: value = DEFAULT_APPROVE_AMOUNT deadline = int(datetime.now(timezone.utc).timestamp()) + 3600 v, r, s = sign_permit(spender=spender, value=value, nonce=nonce, deadline=deadline) ``` The bulk-approval behavior is: ```python def approve_all(amount: Optional[float] = None): """Approve USDC for all casino games.""" ...[truncated 1919 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
script/register.py:24
Finding

Configurable API Origin Can Receive API Keys and Wallet Authorization Signatures

Content
View full analysis
dict: """ Get authentication headers using wallet signature. Only used for registration to prove wallet ownership. """ wallet, signature, timestamp = sign_auth_message() return { "X-Wallet": wallet, "X-Signature": signature, "X-Timestamp": str(timestamp), "Content-Type": "application/json", } ``` The signature is sent directly to the configured origin: ```python response = requests.post( f"{API_URL}/agent/register", headers=get_wallet_signature_header(), json=data, ) ``` The same configurable-origin pattern is used for API-key-authenticated requests in `script/pvp.py:21, 30-40, 89-91`: ```python API_URL = os.getenv("CASINO_API_URL", "https://api.clawdcasino.com/v1") def get_api_key_header() -> dict: api_key = get_api_key() if not api_key: print("Error: CASINO_API_KEY not set") print("Register first with /register to get your API key") sys.exit(1) return { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", } response = requests.post( f"{API_URL}/pvp/request", headers=get_api_key_header(), json=data ) ``` Roulette and unified approval requests use the same construction in `script/roulette.py` and `script/approve.py`. ### Technical Analysis `CASINO_API_URL` can redirect all application requests to an arbitrary origin without validating the URL scheme or hostname. Wallet authentication signatures, wallet addresses, API keys, betting data, and permit signatures are consequently sent wherever the environment variable points. An API URL override can be legitimate for develo ...[truncated 1284 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
script/wallet_gen.py:48
Finding

Wallet Private Keys and API Keys Are Stored and Printed in Plaintext

Content
View full analysis
bool: """Save wallet private key to .env file. Returns True on success.""" try: content = "" if env_path.exists(): content = env_path.read_text() lines = content.splitlines() updated = False new_lines = [] for line in lines: if line.startswith("CASINO_WALLET_KEY="): new_lines.append(f"CASINO_WALLET_KEY={private_key}") updated = True else: new_lines.append(line) if not updated: if new_lines and new_lines[-1]: new_lines.append("") new_lines.append(f"CASINO_WALLET_KEY={private_key}") env_path.write_text("\n".join(new_lines) + "\n") return True ``` The complete private key is printed regardless of whether `--save` is used: ```python private_key = account.key.hex() address = account.address print() print("New wallet generated!") print() print(f" Address: {address}") print(f" Private Key: {private_key}") print() if save_to_env: if save_wallet_to_env(private_key, env_path): print(f"Wallet saved to: {env_path}") print() else: print("Could not save automatically. Add manually:") print(f" export CASINO_WALLET_KEY={private_key}") print() else: print("=" * 60) print("TIP: Use --save to automatically save to .env!") print(" /wallet-gen --save") print("=" * 60) print() print("Or add manually:") print(f" export CASINO_WALLET_KEY={private_key}") ``` API keys are handled similarly in `script/register.py:56-82, 132-153`, including plaintext `.env` writes and full output to stdout. ### Technical Analysis ...[truncated 1412 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
script/wallet.py:39
Finding

Wallet Authentication Signatures Are Not Bound to an Action, Origin, or Server Nonce

Content
View full analysis
Tuple[str, str, int]: """ Sign an authentication message for API calls. Returns (wallet, signature, timestamp). The message format is: "ClawdCasino:{timestamp}" Timestamp must be within 5 minutes of server time. """ wallet = get_wallet() if not wallet: raise ValueError("CASINO_WALLET_KEY not set") timestamp = int(time.time()) message = f"ClawdCasino:{timestamp}" message_hash = encode_defunct(text=message) signed = wallet.sign_message(message_hash) return (wallet.address.lower(), signed.signature.hex(), timestamp) ``` ### Technical Analysis The authentication message includes only a generic application name and a client-generated timestamp. It does not bind the signature to: - A server-issued one-time nonce - The intended API origin - The endpoint or action - The HTTP method - The request body - The chain ID - A specific expiration time Consequently, the same captured signature may be accepted by multiple wallet-signature endpoints during the server's timestamp acceptance window. Client-generated timestamps alone do not provide strong replay protection because the server has no unique challenge to invalidate after use. ### Attack Path 1. An attacker observes or captures the `X-Wallet`, `X-Signature`, and `X-Timestamp` headers through a malicious configured API, logging system, or compromised transport endpoint. 2. Before the server's acceptance window expires, the attacker replays the headers. 3. Because the signature is not bound to a specific method, endpoint, or body, the attacker attempts to use it against another wallet-authenticated request. 4. If server-side authorization relies only on recovering the wallet and validating the timestamp, th ...[truncated 418 chars]
Remediation
View remediation
Vulnerability Patterns
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (87)

Tainted flow: 'API_URL' from os.getenv (line 25, 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 · script/approve.py (reported line 60)May include surrounding context.

python
def get_games() -> list:
    """Get list of all games that can be approved."""
    response = requests.get(f"{API_URL}/approve/game", headers=get_api_key_header())

    if response.status_code != 200:
        # Fallback to known games if endpoint doesn't exist yet

Tainted flow: 'API_URL' from os.getenv (line 25, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

This code fetches the permit nonce and, critically, the spender address from a remote API and then signs an EIP-2612 permit for that spender without any local verification. If the API endpoint is compromised or the API_URL is redirected, the wallet can be tricked into granting USDC allowance to an attacker-controlled address, which is especially dangerous given the very large default approval amount.

Content

Scanner excerpt · script/approve.py (reported line 87)May include surrounding context.

python
return False

    # Get permit nonce and game address from unified API
    response = requests.get(
        f"{API_URL}/approve/{game_name}/permit-nonce", headers=get_api_key_header()
    )
    if response.status_code != 200:

Tainted flow: 'API_URL' from os.getenv (line 25, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Confidence
93% confidence
Finding

The POST target is derived from an environment-configurable API_URL, and the request includes wallet-authentication material plus the signed permit payload. If API_URL is redirected to an attacker-controlled endpoint, the attacker can harvest wallet signatures and permit metadata and submit or replay them depending on the token/contract semantics and backend workflow.

Content

Scanner excerpt · script/approve.py (reported line 123)May include surrounding context.

python
"s": "0x" + s.hex(),
    }

    response = requests.post(
        f"{API_URL}/approve/{game_name}",
        headers=get_wallet_signature_header(),
        json=permit_data,

Tainted flow: 'API_URL' from os.getenv (line 21, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Confidence
95% confidence
Finding

The request URL is derived from the CASINO_API_URL environment variable and the code sends a bearer API key in the Authorization header to that destination. If an attacker can influence the environment or deployment configuration, they can redirect the request to an attacker-controlled server and capture the API key or manipulate returned balance/approval data.

Content

Scanner excerpt · script/balance.py (reported line 49)May include surrounding context.

python
print("Error: CASINO_WALLET_KEY not set")
        sys.exit(1)

    response = requests.get(f"{API_URL}/agent/me", headers=get_api_key_header())
    result = response.json()

    if not result.get("success"):

Tainted flow: 'API_URL' from os.getenv (line 21, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Confidence
94% confidence
Finding

The API base URL is taken from the CASINO_API_URL environment variable and then used for authenticated requests carrying the Bearer API key. If an attacker can influence the environment, they can redirect traffic to an attacker-controlled endpoint and capture credentials or manipulate betting operations. In this skill, that risk is amplified because requests can trigger financial actions.

Content

Scanner excerpt · script/pvp.py (reported line 89)May include surrounding context.

python
"deadline": dl.isoformat(),
    }

    response = requests.post(
        f"{API_URL}/pvp/request", headers=get_api_key_header(), json=data
    )
    result = response.json()

Tainted flow: 'API_URL' from os.getenv (line 21, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Confidence
91% confidence
Finding

This request uses an environment-controlled API_URL together with the API key header. A malicious or compromised environment can reroute the request to a hostile server and exfiltrate the Bearer token, enabling account access and fraudulent operations.

Content

Scanner excerpt · script/pvp.py (reported line 109)May include surrounding context.

python
def list_open_bet():
    """List all bet requests waiting for quotes."""
    response = requests.get(f"{API_URL}/pvp/open", headers=get_api_key_header())
    result = response.json()

    if result.get("success"):

Tainted flow: 'API_URL' from os.getenv (line 21, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Confidence
94% confidence
Finding

Submitting quotes sends authenticated betting data to a URL derived from an untrusted environment variable. If API_URL is altered, the tool can leak the API key and allow an attacker to spoof quote handling for financial transactions.

Content

Scanner excerpt · script/pvp.py (reported line 148)May include surrounding context.

python
"ttl_minutes": ttl,
    }

    response = requests.post(
        f"{API_URL}/pvp/quote", headers=get_api_key_header(), json=data
    )
    result = response.json()

Tainted flow: 'API_URL' from os.getenv (line 21, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

The code queries quote data from an environment-controlled URL while attaching the API key. Redirecting this request to an attacker-controlled host can expose credentials and let the attacker return deceptive quote data to influence user actions.

Content

Scanner excerpt · script/pvp.py (reported line 169)May include surrounding context.

python
def list_quotes(bet_id: str):
    """List all quotes for a bet request."""
    response = requests.get(
        f"{API_URL}/pvp/quote/{bet_id}", headers=get_api_key_header()
    )
    result = response.json()

Tainted flow: 'API_URL' from os.getenv (line 21, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Confidence
95% confidence
Finding

Accepting a quote is a state-changing financial action, yet the destination host is environment-controlled. An attacker who can set API_URL could both steal the API key and induce the client to send sensitive acceptance actions to a rogue service.

Content

Scanner excerpt · script/pvp.py (reported line 207)May include surrounding context.

python
data = {"quote_id": quote_id}

    response = requests.post(
        f"{API_URL}/pvp/quote/accept", headers=get_api_key_header(), json=data
    )
    result = response.json()

Tainted flow: 'API_URL' from os.getenv (line 21, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Confidence
93% confidence
Finding

This withdrawal action sends authenticated data to a URL based on an untrusted environment variable. A hostile endpoint could capture the API key and manipulate client behavior around fund-related operations.

Content

Scanner excerpt · script/pvp.py (reported line 230)May include surrounding context.

python
def withdraw_quote(quote_id: str):
    """Withdraw your open quote."""
    response = requests.post(
        f"{API_URL}/pvp/quote/withdraw",
        headers=get_api_key_header(),
        json={"quote_id": quote_id},

Tainted flow: 'API_URL' from os.getenv (line 21, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

The retrieve call discloses wallet-linked betting information to a configurable endpoint while using the Bearer token. In the context of a casino/betting skill, leaking account and activity metadata to an attacker-controlled server increases both privacy and account-takeover risk.

Content

Scanner excerpt · script/pvp.py (reported line 250)May include surrounding context.

python
print("Error: CASINO_WALLET_KEY not set")
        sys.exit(1)

    response = requests.get(
        f"{API_URL}/pvp/retrieve",
        headers=get_api_key_header(),
        params={

Tainted flow: 'API_URL' from os.getenv (line 21, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Bet history retrieval combines account data and authorization with an environment-supplied host. A malicious host can collect sensitive account history and harvest the API key for later abuse.

Content

Scanner excerpt · script/pvp.py (reported line 288)May include surrounding context.

python
print("Error: CASINO_WALLET_KEY not set")
        sys.exit(1)

    response = requests.get(
        f"{API_URL}/pvp/retrieve",
        headers=get_api_key_header(),
        params={

Tainted flow: 'API_URL' from os.getenv (line 21, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Confidence
94% confidence
Finding

Cancelling a bet is a state-changing authenticated operation sent to an environment-controlled endpoint. That makes credential exfiltration and transaction manipulation possible if runtime configuration is compromised.

Content

Scanner excerpt · script/pvp.py (reported line 327)May include surrounding context.

python
def cancel_bet(bet_id: str):
    """Cancel your bet request (only if no quotes accepted yet)."""
    response = requests.post(
        f"{API_URL}/pvp/cancel",
        headers=get_api_key_header(),
        json={"bet_id": bet_id},

Tainted flow: 'API_URL' from os.getenv (line 21, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

The client sends its API key to a host derived from CASINO_API_URL when fetching account stats. If that URL is attacker-controlled, the token can be stolen and account metadata disclosed.

Content

Scanner excerpt · script/pvp.py (reported line 342)May include surrounding context.

python
def show_stat():
    """Show your betting statistics."""
    response = requests.get(f"{API_URL}/agent/me", headers=get_api_key_header())
    result = response.json()

    if result.get("success"):

Tainted flow: 'API_URL' from os.getenv (line 21, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Balance and allowance information is fetched from an environment-controlled endpoint with authentication attached. In a financial skill, exposing balance data and the API key to a rogue server is materially dangerous.

Content

Scanner excerpt · script/pvp.py (reported line 362)May include surrounding context.

python
def show_balance():
    """Show USDC balance and approval status for betting."""
    response = requests.get(f"{API_URL}/agent/me", headers=get_api_key_header())
    result = response.json()

    if result.get("success"):

Tainted flow: 'API_URL' from os.getenv (line 21, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Confidence
97% confidence
Finding

The permit nonce and spender are fetched from an environment-controlled API, and those values directly influence the EIP-2612 permit that the wallet signs. A malicious endpoint could provide an attacker-controlled spender so the user signs approval for the wrong contract, enabling token theft; this is especially severe because the default amount is extremely large.

Content

Scanner excerpt · script/pvp.py (reported line 401)May include surrounding context.

python
sys.exit(1)

    # Get permit nonce from API
    response = requests.get(f"{API_URL}/pvp/permit-nonce", headers=get_api_key_header())
    if response.status_code != 200:
        print("Error: Could not get permit nonce")
        return

Tainted flow: 'API_URL' from os.getenv (line 21, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Confidence
98% confidence
Finding

The approval submission is sent to an environment-controlled API using wallet-signature-based headers after the client signs a permit. If API_URL is malicious, the attacker can orchestrate the approval flow, harvest signature material/metadata, and abuse the signed permit—made worse by the large default approval amount and direct financial consequence.

Content

Scanner excerpt · script/pvp.py (reported line 436)May include surrounding context.

python
}

    # /approve requires wallet signature (not API key) since we're proving wallet ownership for the permit
    response = requests.post(
        f"{API_URL}/pvp/approve",
        headers=get_wallet_signature_header(),
        json=permit_data,

Tainted flow: 'API_URL' from os.getenv (line 24, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Confidence
95% confidence
Finding

The registration endpoint is derived from the CASINO_API_URL environment variable and is used to send wallet identity and a signed authentication message. If an attacker can influence the environment, they can redirect this request to an attacker-controlled server and capture the wallet address, signature, timestamp, and any returned API key, which is especially risky because this script then trusts the response and may store or display the returned secret.

Content

Scanner excerpt · script/register.py (reported line 113)May include surrounding context.

python
data["skill_version"] = skill_version

    print(f"Registering wallet: {wallet}")
    response = requests.post(
        f"{API_URL}/agent/register",
        headers=get_wallet_signature_header(),
        json=data,

Tainted flow: 'API_URL' from os.getenv (line 18, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Confidence
94% confidence
Finding

The base URL is taken from an environment variable and used directly for an authenticated POST that includes the Bearer API key. If an attacker can influence CASINO_API_URL in the runtime environment, they can redirect wagering requests and the Authorization header to an attacker-controlled endpoint, causing credential exfiltration and fraudulent bet submission.

Content

Scanner excerpt · script/roulette.py (reported line 65)May include surrounding context.

python
if bet_value is not None:
        data["bet_value"] = bet_value

    response = requests.post(
        f"{API_URL}/roulette/spin", headers=get_api_key_header(), json=data
    )
    result = response.json()

Tainted flow: 'API_URL' from os.getenv (line 18, 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 · script/roulette.py (reported line 106)May include surrounding context.

python
def show_rule():
    """Show roulette rules and bet types."""
    response = requests.get(f"{API_URL}/roulette/rule")
    result = response.json()

    if result.get("success"):

Tainted flow: 'API_URL' from os.getenv (line 18, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Confidence
93% confidence
Finding

The history endpoint sends an authenticated GET using a host derived from CASINO_API_URL. If that environment variable is attacker-controlled, the Bearer token and account activity queries can be sent to a malicious server, exposing credentials and user gambling history.

Content

Scanner excerpt · script/roulette.py (reported line 138)May include surrounding context.

python
def show_history(limit: int = 20):
    """Show roulette spin history."""
    response = requests.get(
        f"{API_URL}/roulette/history",
        headers=get_api_key_header(),
        params={"limit": limit},

Tainted flow: 'API_URL' from os.getenv (line 18, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Confidence
93% confidence
Finding

This authenticated statistics request will send the API key to whatever host is configured in CASINO_API_URL. In an adversarial or multi-tenant environment, that creates a straightforward token exfiltration path and unauthorized access to account data.

Content

Scanner excerpt · script/roulette.py (reported line 186)May include surrounding context.

python
def show_stat():
    """Show roulette statistics."""
    response = requests.get(f"{API_URL}/roulette/stat", headers=get_api_key_header())
    result = response.json()

    if result.get("success"):

Tainted flow: 'API_URL' from os.getenv (line 18, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Confidence
95% confidence
Finding

The balance/account endpoint includes the Bearer token and retrieves wallet information from a host fully controlled by CASINO_API_URL. An attacker who can set that variable can capture the token and potentially trick the tool into disclosing account metadata or interacting with a rogue backend.

Content

Scanner excerpt · script/roulette.py (reported line 214)May include surrounding context.

python
def show_balance():
    """Show USDC balance and approval status."""
    response = requests.get(f"{API_URL}/agent/me", headers=get_api_key_header())
    result = response.json()

    if result.get("success"):

Tainted flow: 'API_URL' from os.getenv (line 16, 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 · script/version.py (reported line 28)May include surrounding context.

python
Returns True if up to date, False if update needed.
    """
    try:
        response = requests.get(f"{API_URL}/skill/version", timeout=10)
        result = response.json()

        server_version = result.get("version", "unknown")

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding

This skill does more than gameplay: it generates blockchain wallets, may display private keys, and writes secrets to local .env files. In the context of a financial skill handling real-money assets, under-emphasizing secret generation and storage is dangerous because key disclosure or accidental overwrite can lead to irreversible fund loss and credential compromise.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.