Back to skill

Security audit

Clawd Casino

Security checks across malware telemetry and agentic risk

Overview

This crypto casino skill is mostly transparent about its purpose, but it asks the agent to store wallet secrets and sign very large USDC approvals with weak local safeguards.

Review carefully before installing. Use only a fresh wallet with funds you can afford to lose, avoid --save unless you accept plaintext local secret storage, verify CASINO_API_URL points to the real service, and do not sign large USDC approvals unless the spender, amount, token, chain, and destination host are clearly confirmed.

SkillSpector

By NVIDIA
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (41)

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

Critical
Category
Data Flow
Content
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:
Confidence
90% confidence
Finding
response = requests.get( f"{API_URL}/approve/{game_name}/permit-nonce", headers=get_api_key_header() )

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

Critical
Category
Data Flow
Content
"s": "0x" + s.hex(),
    }

    response = requests.post(
        f"{API_URL}/approve/{game_name}",
        headers=get_wallet_signature_header(),
        json=permit_data,
Confidence
95% confidence
Finding
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.post (network output)

Critical
Category
Data Flow
Content
"deadline": dl.isoformat(),
    }

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

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

Critical
Category
Data Flow
Content
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"):
Confidence
88% confidence
Finding
response = requests.get(f"{API_URL}/pvp/open", headers=get_api_key_header())

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

Critical
Category
Data Flow
Content
"ttl_minutes": ttl,
    }

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

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

Critical
Category
Data Flow
Content
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()
Confidence
87% confidence
Finding
response = requests.get( f"{API_URL}/pvp/quote/{bet_id}", headers=get_api_key_header() )

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

Critical
Category
Data Flow
Content
data = {"quote_id": quote_id}

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

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

Critical
Category
Data Flow
Content
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},
Confidence
93% confidence
Finding
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
Content
print("Error: CASINO_WALLET_KEY not set")
        sys.exit(1)

    response = requests.get(
        f"{API_URL}/pvp/retrieve",
        headers=get_api_key_header(),
        params={
Confidence
89% confidence
Finding
response = requests.get( f"{API_URL}/pvp/retrieve", headers=get_api_key_header(), params={ "proposer_wallets": wallet, "statuses": ["REQUEST", "MATC

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

Critical
Category
Data Flow
Content
print("Error: CASINO_WALLET_KEY not set")
        sys.exit(1)

    response = requests.get(
        f"{API_URL}/pvp/retrieve",
        headers=get_api_key_header(),
        params={
Confidence
89% confidence
Finding
response = requests.get( f"{API_URL}/pvp/retrieve", headers=get_api_key_header(), params={ "proposer_wallets": wallet, "statuses": ["SETTLED", "CANC

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

Critical
Category
Data Flow
Content
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},
Confidence
93% confidence
Finding
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
Content
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"):
Confidence
88% confidence
Finding
response = requests.get(f"{API_URL}/agent/me", headers=get_api_key_header())

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

Critical
Category
Data Flow
Content
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"):
Confidence
90% confidence
Finding
response = requests.get(f"{API_URL}/agent/me", headers=get_api_key_header())

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

Critical
Category
Data Flow
Content
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
Confidence
98% confidence
Finding
response = requests.get(f"{API_URL}/pvp/permit-nonce", headers=get_api_key_header())

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

Critical
Category
Data Flow
Content
}

    # /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,
Confidence
97% confidence
Finding
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
Content
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,
Confidence
94% confidence
Finding
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
Content
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()
Confidence
96% confidence
Finding
response = requests.post( f"{API_URL}/roulette/spin", headers=get_api_key_header(), json=data )

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

Critical
Category
Data Flow
Content
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},
Confidence
95% confidence
Finding
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
Content
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"):
Confidence
95% confidence
Finding
response = requests.get(f"{API_URL}/roulette/stat", headers=get_api_key_header())

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

Critical
Category
Data Flow
Content
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"):
Confidence
97% confidence
Finding
response = requests.get(f"{API_URL}/agent/me", headers=get_api_key_header())

Lp3

Medium
Category
MCP Least Privilege
Confidence
96% confidence
Finding
The skill documents capabilities to read environment variables, write to local files, and make network requests, yet it does not declare permissions. That prevents operators and policy systems from understanding that the skill can create wallets, persist secrets, and contact a remote gambling API, which materially increases the risk of hidden secret handling and unintended fund movement.

Tp4

High
Category
MCP Tool Poisoning
Confidence
90% confidence
Finding
The skill is described as a casino gameplay tool, but it also performs sensitive wallet lifecycle operations: generating private keys, storing them, registering identities, and signing approvals. That mismatch can mislead users into invoking what sounds like a game skill while actually granting it custody-like access over credentials and token approvals.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The instructions strongly recommend using `--save` to place a wallet private key and API key into `.env`, but do not provide a prominent warning that this is plaintext local secret storage. Plaintext persistence of blockchain private keys is especially dangerous because compromise directly enables theft of funds and unauthorized signing.

Missing User Warnings

Medium
Confidence
97% confidence
Finding
The default approval amount is 1,000,000 USDC and the command can approve all games without any interactive warning or confirmation. In a casino/betting context, this is especially dangerous because users may run a seemingly routine setup command and unintentionally grant enormous spending authority that could be abused if a game contract or backend is compromised.

Missing User Warnings

Medium
Confidence
99% confidence
Finding
The default approval amount is 1,000,000 USDC, which is extremely large, and the code performs the approval without an explicit just-in-time warning or forced confirmation. In a financial skill, excessive standing allowance materially increases loss if the casino contract, relayer, or signing flow is compromised.

VirusTotal

64/64 vendors flagged this skill as clean.

View on VirusTotal

Static analysis

No suspicious patterns detected.