Back to skill

Security audit

Rhaios Staging

Security checks for vulnerabilities and agentic risk

Overview

The skill is purpose-built for Rhaios staging, but it gives a wallet-signing tool enough authority to approve and relay externally prepared DeFi actions without sufficient local safeguards.

Use only with a dedicated staging/test wallet that holds no valuable production assets. Do not provide production private keys or broad Privy credentials unless you fully trust the Rhaios staging API and relay, and require explicit user review of vault choice, operation, amount, wallet address, and every live signing run.

Vulnerability Patterns
  • 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
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (3)

T09 · Insecure Skill Coding Practices

Error
Location
src/signing.ts:280
Finding
Server-Controlled EIP-7702 Authorization Is Signed Without Chain or Implementation Allowlisting<![CDATA[ ## Vulnerability Details **File Location**: `src/signing.ts:280-298`; `scripts/prepare-sign-execute.ts:366-392`; `scripts/prepare-sign-execute.ts:706-742` **Vulnerability Type**: Untrusted EIP-7702 authorization signing **Risk Level**: High ### Vulnerable Code From `src/signing.ts:280-298`: ```ts if (envelope.eip7702AuthRequest) { const authRequest = envelope.eip7702AuthRequest as { contractAddress?: string; chainId?: number; nonce?: number | string; }; if (!authRequest.contractAddress || typeof authRequest.chainId !== 'number') { throw new Error('intentEnvelope.eip7702AuthRequest is invalid.'); } const txCount = await publicClient.getTransactionCount({ address: signer.address }); const nonce = typeof authRequest.nonce === 'number' ? authRequest.nonce : typeof authRequest.nonce === 'string' ? Number(authRequest.nonce) : Number(txCount); const auth = await signer.signAuthorization({ contractAddress: authRequest.contractAddress as Address, chainId: authRequest.chainId, nonce, }); ``` From `scripts/prepare-sign-execute.ts:366-392`: ```ts let authorization: SetupPayload['authorization'] = null; if (setupType === 'full') { const authRaw = setup.authorization; if (!isRecord(authRaw)) { throw new Error('setup.authorization is missing (required for full setup).'); } const contractAddress = authRaw.contractAddress; if (typeof contractAddress !== 'string' || !ADDRESS_RE.test(contractAddress)) { throw new Error('setup.authorization.contractAddress is missing or invalid.'); } const chainId = authRaw.chainId; if (typeof chainId !== 'number' || !Number.isInteger(chainId) || chainId <= 0) { throw new Error('setup.authorization.chainId is missing or invalid.'); } authorization = { contractAddress: contractAddress as Address, chainId }; } ``` From `scripts/prepare-sign-execute.ts:706-742`: ```ts if (!setupPayload.authorization) { throw new Error('Full setup requi ...[truncated 3718 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Maintain a local allowlist of audited EIP-7702 implementation addresses for each supported chain. 2. Require `authRequest.chainId`, `setup.authorization.chainId`, `envelope.chainId`, and `preflight.chain.chainId` to be identical. 3. Reject authorization requests containing server-selected nonces unless they exactly match a nonce obtained from a trusted, explicitly configured RPC. 4. Do not rely on documentation or relay behavior to enforce fork-only execution. Use a staging-specific chain domain or another cryptographic mechanism that prevents production-chain replay. 5. Decode and validate all associated setup calldata before authorization. 6. Present the delegation target, chain, nonce, and decoded action to the user and require explicit approval before live signing. 7. Authenticate prepare responses or verify them against a locally defined signing policy before invoking either the local signer or Privy. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/prepare-sign-execute.ts:395
Finding
Setup Ticket Is Not Cryptographically or Logically Bound to the Signed Setup Transaction<![CDATA[ ## Vulnerability Details **File Location**: `scripts/prepare-sign-execute.ts:395-452`; `scripts/prepare-sign-execute.ts:678-753` **Vulnerability Type**: Missing integrity validation between setup metadata and signed transaction **Risk Level**: High ### Vulnerable Code From `scripts/prepare-sign-execute.ts:395-452`: ```ts function parseSetupTicket( preparePayload: Record<string, unknown>, expectedWalletAddress: Address, expectedChainId: number, ): SetupTicket { const ticket = preparePayload.setupTicket; if (!isRecord(ticket)) { throw new Error('yield_prepare needsSetup=true but setupTicket is missing.'); } if (ticket.version !== 1) { throw new Error('setupTicket.version must be 1.'); } const walletAddress = ticket.walletAddress; if (typeof walletAddress !== 'string' || !ADDRESS_RE.test(walletAddress)) { throw new Error('setupTicket.walletAddress is missing or invalid.'); } if (walletAddress.toLowerCase() !== expectedWalletAddress.toLowerCase()) { throw new Error( `setupTicket.walletAddress (${walletAddress}) does not match wallet address (${expectedWalletAddress}).`, ); } const chainId = ticket.chainId; if (typeof chainId !== 'number' || !Number.isInteger(chainId) || chainId <= 0) { throw new Error('setupTicket.chainId is missing or invalid.'); } if (chainId !== expectedChainId) { throw new Error(`setupTicket.chainId (${chainId}) does not match selected chain (${expectedChainId}).`); } const implementation = ticket.implementation; if (typeof implementation !== 'string' || !ADDRESS_RE.test(implementation)) { throw new Error('setupTicket.implementation is missing or invalid.'); } const initCalldataHash = ticket.initCalldataHash; if (typeof initCalldataHash !== 'string' || !HEX32_RE.test(initCalldataHash)) { throw new Error('setupTicket.initCalldataHash is missing or invalid.'); } const expiresAt = ticket.expiresAt; if (typeof expiresAt !== 'string' || N ...[truncated 3031 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Before signing, require exact equality between the ticket and setup payload: - `setupTicket.implementation` must equal the approved authorization implementation. - `setupTicket.initCalldataHash` must equal `keccak256(setupPayload.initCalldata)`. - `setupTicket.setupType` must equal `setupPayload.setupType`. - Wallet and chain fields must match both the transaction and preflight context. 2. Reject expired tickets and impose a short maximum future validity period. 3. Require the setup ticket to be signed by a pinned server key and verify that signature locally. 4. Decode `initCalldata` and allow only documented setup selectors, module addresses, and parameter combinations. 5. Pin all approved implementation and module addresses locally by chain. 6. Remove fallback tickets unless they provide equivalent authenticated integrity and are accepted only for locally verified setup transactions. 7. Treat relay validation as defense in depth rather than the primary control. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
src/signing.ts:231
Finding
Intent Signatures Are Not Bound to the User-Requested Operation or Locally Recomputed Merkle Commitment<![CDATA[ ## Vulnerability Details **File Location**: `src/signing.ts:231-332` **Vulnerability Type**: Insufficient semantic validation of server-generated signing payloads **Risk Level**: High ### Vulnerable Code From `src/signing.ts:231-278`: ```ts const envelope = parseIntentEnvelope(preparePayload.intentEnvelope); if (envelope.chainId !== chain.chainId) { throw new Error( `intentEnvelope.chainId (${envelope.chainId}) does not match selected chain (${chain.chainId}).`, ); } const firstOp = envelope.userOps[0] as Record<string, unknown>; const firstUserOperation = firstOp.userOperation as Record<string, unknown> | undefined; const opSender = typeof firstUserOperation?.sender === 'string' ? firstUserOperation.sender : ''; if (!/^0x[a-fA-F0-9]{40}$/.test(opSender)) { throw new Error('intentEnvelope.userOps[0].userOperation.sender is missing or invalid.'); } if (opSender.toLowerCase() !== signer.address.toLowerCase()) { throw new Error( `userOperation.sender (${opSender}) does not match signer address (${signer.address}).`, ); } const signing = parseSigningPayload(envelope.signing); if (signing.domain.chainId !== chain.chainId) { throw new Error( `signing.domain.chainId (${signing.domain.chainId}) does not match selected chain (${chain.chainId}).`, ); } if (signing.message.chainId !== envelope.chainId) { throw new Error( `signing.message.chainId (${signing.message.chainId}) does not match envelope chainId (${envelope.chainId}).`, ); } if (signing.message.merkleRoot.toLowerCase() !== envelope.merkleRoot.toLowerCase()) { throw new Error( `signing.message.merkleRoot (${signing.message.merkleRoot}) does not match envelope merkleRoot (${envelope.merkleRoot}).`, ); } ``` From `src/signing.ts:300-332`: ```ts // Compute the on-chain SuperValidator-format signature for every UserOp. // This is what goes into userOp.signature when submitting to the bundler. // Stored as a separate field so the intent Merkle (which uses sign ...[truncated 3063 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Recompute the intent merkle root locally from a canonical serialization of every returned UserOperation. 2. Validate the sender of every UserOperation, not only the first. 3. Decode all UserOperation calldata before signing. 4. Bind decoded actions to the original request: - Operation type must match. - Vault and token addresses must match approved mappings. - Amount or share values must match the user’s request and limits. - Recipients and beneficiaries must equal the configured wallet unless explicitly approved. - Native value and token approvals must be tightly bounded. 5. Pin the entry point, validator, EIP-712 verifier, implementation, and supported vault contracts for every chain. 6. Reject unexpected additional operations and impose a strict maximum envelope size. 7. Present a human-readable summary of every decoded action and require explicit confirmation for live execution. 8. Authenticate prepare responses as defense in depth, while retaining complete local semantic verification. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (10)

Lp3

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding
The skill clearly performs network access to the Rhaios staging API and consumes sensitive environment variables, yet it does not declare any explicit tool scope or allowed-tools boundary. That makes the skill's effective capabilities less transparent to the host and user, increasing the risk of overbroad execution, unexpected secret access, or accidental invocation with privileges that were not intentionally granted.

Vague Triggers

Medium
Confidence
93% confidence
Finding
The trigger text is broad enough to match generic DeFi yield, vault, redeem, and rebalancing requests, not just narrowly scoped Rhaios staging operations. This can cause the skill to activate in contexts where the user did not intend to use this external staging API, leading to unintended transaction preparation, secret usage, or external data transmission.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
# Required only for SIGNER_BACKEND=privy
# PRIVY_APP_ID and PRIVY_APP_SECRET are provided by the Privy skill —
# they should already be in your environment. Do NOT ask the user for these.
PRIVY_WALLET_ID=<wallet-id>
PRIVY_WALLET_ADDRESS=<0x-wallet-address>
Confidence
80% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

External Transmission

Medium
Category
Data Exfiltration
Content
Check test RPC health first:

```bash
  https://api.staging.rhaios.com/v1/testing/fork-status
```

Then fund your wallet:
Confidence
50% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
```bash
  -H "Content-Type: application/json" \
  https://api.staging.rhaios.com/v1/testing/fund-wallet \
  -d '{
    "chain": "base",
    "walletAddress": "0xYourAgentAddress",
Confidence
81% confidence
Finding
This endpoint transmits wallet addresses and funding parameters to a remote service that can alter wallet balances on managed test RPC forks. While consistent with the skill's stated purpose, it still performs external state-changing operations involving user-linked wallet identifiers, so misuse or unexpected invocation could affect testing environments, leak wallet metadata, or normalize sending sensitive operational details off-box.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The function hardcodes fork-only relay mode to always enabled and states it cannot be disabled, which can silently alter execution semantics from what users or calling agents may expect. In a DeFi transaction flow, forcing staging/fork relay behavior without an explicit runtime check or user-visible confirmation can cause transactions to be prepared, signed, or broadcast against an unintended environment, undermining informed consent and increasing the risk of mis-execution or misleading operational results.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
This code sends the contents of `args` to remote staging API endpoints via `fetch`, including query parameters for GET requests and JSON bodies for POST requests. While the file has internal comments, it provides no user-facing warning, confirmation, or logging about transmitting user or system data, which matches the missing-warning criterion for code files.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"prepare-sign-execute": "bun run scripts/prepare-sign-execute.ts"
  },
  "dependencies": {
    "@privy-io/node": "^0.9.0",
    "tsx": "^4.19.0",
    "viem": "^2.46.3"
  },
Confidence
89% confidence
Finding
The dependency uses a caret range, which permits automatic installation of newer minor or patch releases that were not explicitly reviewed by the skill author. In a security-sensitive DeFi toolkit that performs signing and transaction execution, a compromised or breaking upstream release could alter transaction construction, leak secrets, or introduce supply-chain compromise into the agent environment.

Unpinned Dependencies

Low
Category
Supply Chain
Content
},
  "dependencies": {
    "@privy-io/node": "^0.9.0",
    "tsx": "^4.19.0",
    "viem": "^2.46.3"
  },
  "license": "MIT",
Confidence
84% confidence
Finding
Using a caret version for tsx allows unreviewed updates to be pulled during installation, creating a supply-chain and reproducibility risk. While tsx is primarily a tooling/runtime dependency, compromise of developer tooling can still execute arbitrary code in install or runtime contexts.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"dependencies": {
    "@privy-io/node": "^0.9.0",
    "tsx": "^4.19.0",
    "viem": "^2.46.3"
  },
  "license": "MIT",
  "repository": {
Confidence
93% confidence
Finding
The viem dependency is unpinned via a caret range, so new upstream releases may be consumed without code review. Because viem is directly involved in blockchain interaction and likely transaction/signing flows, a malicious or flawed update could change encoding, destination handling, signing semantics, or other transaction-critical behavior in a DeFi skill.

Static analysis

Detected: suspicious.env_credential_access, suspicious.exposed_secret_literal

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
src/preflight.ts:32

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
scripts/prepare-sign-execute.ts:591

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
src/preflight.ts:321