Back to skill

Security audit

Element NFT Trader

Security checks for vulnerabilities and agentic risk

Overview

This is a real NFT trading skill with disclosed wallet use, but it signs high-impact blockchain transactions from remotely supplied calldata without enough local validation.

Install only with a dedicated low-value wallet, assume every buy, sale, offer, cancellation, and approval can move real assets or grant lasting marketplace permissions, and verify transaction details in an external wallet or explorer before funding the wallet. The main unresolved risk is not hidden persistence, but insufficient local validation before signing remote marketplace transaction data.

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
scripts/code/src/index.ts:224
Finding
Remote API Response Is Signed and Broadcast Without Local Transaction Validation<![CDATA[ ## Vulnerability Details **File Location**: `scripts/code/src/index.ts:224-242`; transaction sink at `scripts/code/src/signer/Web3Signer.ts:55-86` **Vulnerability Type**: Unvalidated remotely supplied blockchain transaction **Risk Level**: Critical ### Vulnerable Code ```typescript const tradeData = await queryTradeData( account, [ { orderId: params.order.orderId, takeCount, tokenId: params.assetId?.toString(), }, ], this.apiOption, ); const call: LimitedCallSpec = { from: account, to: tradeData.to, value: tradeData.value, data: tradeData.data, gasPrice: params.gasPrice, maxPriorityFeePerGas: params.maxPriorityFeePerGas, maxFeePerGas: params.maxFeePerGas, }; return this.web3Signer.ethSend(call); ``` The same pattern is used by `batchBuyWithETH`: ```typescript const tradeData = await queryTradeData(taker, list, this.apiOption); const call: LimitedCallSpec = { from: taker, to: tradeData.to, value: tradeData.value, data: tradeData.data, gasPrice: params.gasPrice, maxPriorityFeePerGas: params.maxPriorityFeePerGas, maxFeePerGas: params.maxFeePerGas, }; return this.web3Signer.ethSend(call); ``` The transaction is then signed without validating its destination, value, or calldata: ```typescript const transactionRequest: any = { from: call.from, to: call.to, data: call.data } if (call.value && ethers.BigNumber.from(call.value).gt(0)) { transactionRequest.value = ethers.BigNumber.from(call.value) } const signer = await this.getSigner(call.from) if (call.maxFeePerGas && call.maxPriorityFeePerGas) { transactionRequest.maxFeePerGas = ethers.BigNumber.from(call.maxFeePerGas) transactionRequest.maxPriorityFeePerGas = ethers.BigNumber.from(call.maxPriorityFeePerGas) } else if (call.gasPrice) { transactionRequest.gasPrice = ethers.BigNumber.from(call.gasPrice) } else { if (!(this.signer instanceof ethers.providers.Web3Provider)) { const gas = await estimateGas(this.chainId) ...[truncated 3247 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Maintain a chain-specific allowlist of trusted Element exchange and helper contract addresses. 2. Reject any transaction whose `to` address is not the expected deployment for the selected operation and network. 3. Decode `tradeData.data` locally using the bundled ABI before signing. 4. Verify the function selector against a per-operation allowlist. 5. Verify decoded order IDs, quantities, NFT addresses, token IDs, payment tokens, recipients, and prices against the user-confirmed input. 6. Calculate the maximum permitted native value locally and reject any higher `tradeData.value`. 7. Fetch the provider chain ID immediately before signing and compare it with the selected network. 8. Prefer constructing transaction calldata locally from audited contract ABIs instead of accepting opaque remote calldata. 9. Run `callStatic` or `eth_call` simulation and inspect asset balance changes before broadcasting. 10. Generate the confirmation preview from the final decoded transaction, not merely from the initial order object. 11. Apply explicit value caps and reject unlimited or unexplained asset movements. 12. Mirror all changes in the prebuilt runtime under `scripts/lib/`, since that is the documented execution path. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/code/src/index.ts:170
Finding
Order Side Is Not Semantically Enforced Before Executing Buy or Accept-Offer Operations<![CDATA[ ## Vulnerability Details **File Location**: `scripts/code/src/index.ts:170-214`; affected dispatch and validation paths in `scripts/entry.ts` **Vulnerability Type**: Missing transaction-intent and order-side validation **Risk Level**: High ### Vulnerable Code ```typescript public async fillOrder( params: FillOrderParams, ): Promise<TransactionResponse> { if (params.order.standard?.toString().toLowerCase() != Standard.ElementEx) { throw Error( `fillOrder failed, standard(${params.order.standard}) is not supported`, ); } const account = await this.web3Signer.getCurrentAccount(); const takeCount = params.quantity ? Number(params.quantity) || 1 : 1; if ( params.order.side === OrderSide.SellOrder || params.order.side == "sell" ) { if (toStandardERC20Token(params.order.paymentToken) !== NULL_ADDRESS) { const providedDecimals = Number(params.order.paymentTokenDecimals); if (!Number.isFinite(providedDecimals)) { throw Error( "fillOrder failed, ERC20-priced orders require `paymentTokenDecimals`. Look it up from the payment token reference and pass it explicitly.", ); } const decimals = providedDecimals; const payValue = takeCount * Number(params.order.price); const value = ethers.utils.parseUnits(payValue.toString(), decimals); console.log( "approve: " + JSON.stringify(params), JSON.stringify(value), ); await approveERC20( this.web3Signer, params.order.paymentToken, value, params, ); } } else { await setApproveForAll( this.web3Signer, params.order.contractAddress, params, ); } ``` The entry-point validation only verifies that `side` is present: ```typescript validateOrderFields(operationType, order, "acceptOfferOrder.order", [ { field: "orderId" }, { field: "contractAddress" }, { field: "standard" }, { field: "schema" }, { field: "side", deta ...[truncated 3657 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pass the intended operation into `fillOrder` or expose separate strongly typed methods such as `fillSellOrder` and `fillBuyOrder`. 2. For `buy`, require the canonical sell-side enum value and reject all other values. 3. For `acceptOffer`, require the canonical buy-side enum value and reject all other values. 4. Normalize only explicitly supported numeric and string side representations; reject unknown values instead of using a generic `else` branch. 5. Retrieve the authoritative order by ID immediately before execution and compare its side, maker, taker, contract, token ID, schema, payment token, price, expiration, and quantity with the confirmed order. 6. Parse and validate `exchangeData`, then ensure it matches all top-level order fields. 7. Verify signatures and order hashes locally where the protocol permits. 8. Never issue ERC20 or NFT approvals until all semantic and structural order checks have passed. 9. Include the exact economic direction in the final preview: assets spent, assets received, order side, approvals, and maximum amounts. 10. Add tests proving that `buy` rejects buy-side orders, `acceptOffer` rejects sell-side orders, and unknown side values cannot trigger approvals. 11. Update both TypeScript source and the shipped `scripts/lib/` JavaScript runtime. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/code/src/contracts/config.ts:220
Finding
Remote RPC Endpoint Configuration Is Accepted Without URL or Chain Validation<![CDATA[ ## Vulnerability Details **File Location**: `scripts/code/src/contracts/config.ts:220-300`; endpoint use at `scripts/entry.ts:480-487` **Vulnerability Type**: Untrusted remote configuration controlling wallet RPC connectivity **Risk Level**: Medium ### Vulnerable Code ```typescript export const RPC_CONF_API_URL = 'https://api.element.market/v1/quote/rpcConfInfo' export async function fetchRpcConfigs(): Promise<RpcConfInfo[]> { try { const response = await fetch(RPC_CONF_API_URL) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } const result = await response.json() as RpcConfResponse if (result.code !== 0) { throw new Error(`API error: ${result.status}`) } return result.data } catch (error) { console.error('Failed to fetch RPC configs:', error) throw error } } export async function getCachedRpcConfigs(): Promise<Map<number, RpcConfInfo>> { const now = Date.now() if (cachedRpcConfigs && now - lastFetchTime < CACHE_DURATION) { return cachedRpcConfigs } const configs = await fetchRpcConfigs() const filteredConfigs = configs.filter(config => !config.isWalletPriority) cachedRpcConfigs = new Map(filteredConfigs.map(config => [config.chainMId, config])) Object.entries(RPC_URLS).forEach(([chainMId, rpcUrl]) => { if (!cachedRpcConfigs!.has(parseInt(chainMId))) { cachedRpcConfigs!.set(parseInt(chainMId), { chainMId: parseInt(chainMId), rpcUrl, isWalletPriority: false }) } }) lastFetchTime = now return cachedRpcConfigs } export async function getRpcUrlFromRemote(chainMId: number): Promise<string | undefined> { try { const configs = await getCachedRpcConfigs() const config = configs.get(chainMId) return config?.rpcUrl } catch (error) { console.error(`Failed to get RPC URL for chain ${chainMId} from remote:`, error) return undefined } } ``` The returned URL is used directly to creat ...[truncated 3155 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Prefer an administrator-configured RPC URL supplied through an explicit environment variable. 2. Maintain an audited per-chain allowlist of HTTPS RPC origins. 3. Reject non-HTTPS URLs, embedded credentials, unexpected ports, fragments, and malformed URLs. 4. Resolve and reject loopback, link-local, private, multicast, and metadata-service addresses unless explicitly required. 5. Call `eth_chainId` after provider initialization and require an exact match with the selected network. 6. Pin RPC configuration responses cryptographically if remote configuration remains necessary. 7. Use a locally bundled fallback for every supported chain rather than silently accepting arbitrary remote providers. 8. Fail closed when no trusted RPC is available. 9. Do not treat RPC simulation or receipts as sufficient transaction authorization. 10. Document that wallet addresses, transaction data, and signed transactions are disclosed to the selected RPC provider. 11. Apply the same validation in the shipped `scripts/lib/code/src/contracts/config.js` runtime. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • System Prompt LeakageDirect Leakage, Indirect Extraction, Tool-Based Exfiltration
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (85)

Tp4

High
Category
MCP Tool Poisoning
Confidence
94% confidence
Finding
Direct ERC721/ERC1155 transfers and generic token utility behavior exceed the described order/query/cancel scope of the skill. Because the skill operates with a configured private key, any hidden or auxiliary asset-transfer functionality materially increases the risk of unauthorized or accidental loss of NFTs.

Tp4

High
Category
MCP Tool Poisoning
Confidence
94% confidence
Finding
Direct ERC721/ERC1155 transfers and generic token utility behavior exceed the described order/query/cancel scope of the skill. Because the skill operates with a configured private key, any hidden or auxiliary asset-transfer functionality materially increases the risk of unauthorized or accidental loss of NFTs.

Tp4

High
Category
MCP Tool Poisoning
Confidence
94% confidence
Finding
Direct ERC721/ERC1155 transfers and generic token utility behavior exceed the described order/query/cancel scope of the skill. Because the skill operates with a configured private key, any hidden or auxiliary asset-transfer functionality materially increases the risk of unauthorized or accidental loss of NFTs.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
Direct ERC721/ERC1155 transfers and generic token utility behavior exceed the described order/query/cancel scope of the skill. Because the skill operates with a configured private key, any hidden or auxiliary asset-transfer functionality materially increases the risk of unauthorized or accidental loss of NFTs.

Tp4

High
Category
MCP Tool Poisoning
Confidence
94% confidence
Finding
Direct ERC721/ERC1155 transfers and generic token utility behavior exceed the described order/query/cancel scope of the skill. Because the skill operates with a configured private key, any hidden or auxiliary asset-transfer functionality materially increases the risk of unauthorized or accidental loss of NFTs.

Ae1

High
Category
analysis-evasion
Content
nclude prebuilt JavaScript under `scripts/lib/`, so the runtime path uses `node scripts/lib/entry.js` instead of compiling on the user's machine.
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
nclude prebuilt JavaScript under `scripts/lib/`, so the runtime path uses `node scripts/lib/entry.js` instead of compiling on the user's machine.
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
nclude prebuilt JavaScript under `scripts/lib/`, so the runtime path uses `node scripts/lib/entry.js` instead of compiling on the user's machine.
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
nclude prebuilt JavaScript under `scripts/lib/`, so the runtime path uses `node scripts/lib/entry.js` instead of compiling on the user's machine.
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- `scripts/entry.ts`: TypeScript source for the main executor
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- `scripts/entry.ts`: TypeScript source for the main executor
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- `scripts/entry.ts`: TypeScript source for the main executor
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Direct Prompt Extraction

High
Category
System Prompt Leakage
Content
| **Standard** | `[STANDARD]` |
| **Expires At** | `[READABLE_EXPIRATION_TIME]` |

Display rules:

- If `count > 1`, do not present the result as if there were only one order
- When multiple orders are returned, list each order or explicitly state that the display is truncated
Confidence
85% confidence
Finding
Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Direct Prompt Extraction

High
Category
System Prompt Leakage
Content
- normalized order summaries
- `orders[].expirationTime` as a readable UTC time string

Result display rules:

- If `count > 1`, do not describe the result as a single order
- When multiple orders are returned, list each order or explicitly say the output is truncated
Confidence
85% confidence
Finding
Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Direct Prompt Extraction

High
Category
System Prompt Leakage
Content
Run with `node scripts/lib/entry.js "$INPUT"`.

## Display Rules For Offers

- If `side=0` and `sale_kind=7`, this is a collection-wide offer
- Do not describe a `sale_kind=7` order as token `#0` even if the returned `tokenId` is `0`
Confidence
85% confidence
Finding
Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Description-Behavior Mismatch

High
Confidence
98% confidence
Finding
This ABI includes capabilities far beyond the declared Element NFT trading scope, including generic asset transfer operations and administrative controls. In an agent skill context, exposing out-of-scope methods increases the chance that upstream code, prompt injection, or tool misuse could invoke dangerous functions that move user assets or alter contract behavior.

Context-Inappropriate Capability

High
Confidence
99% confidence
Finding
The ABI exposes upgrade, migration, rollback, method registration, and ownership-management functions such as extend, migrate, registerMethods, rollback, and transferOwnership. These are highly privileged operations unrelated to ordinary NFT trading, and if an agent or integration can reach them, they could reconfigure execution paths, transfer control, or facilitate asset loss and persistent compromise.

Missing User Warnings

High
Confidence
92% confidence
Finding
`cancelAllOrders` calls `incrementHashNonce()` and immediately submits the transaction, which appears to invalidate all existing orders for the account. Because this is a broad, destructive operation and the code shows no confirmation, warning log, or explanatory documentation, it lacks the user disclosure required for safety-critical actions.

Context-Inappropriate Capability

High
Confidence
97% confidence
Finding
The SDK exposes generic `transferERC721` and `transferERC1155` methods that can move NFTs directly, which exceeds the stated Element trading/order-management scope. In an agent skill context, this broadens capability from marketplace actions to arbitrary asset exfiltration, enabling irreversible transfers to attacker-controlled addresses if the agent is prompted or misused.

Missing User Warnings

High
Confidence
97% confidence
Finding
The NFT transfer methods perform direct on-chain transfers with no built-in warning, recipient validation, or confirmation checkpoint. Because NFT transfers are typically irreversible, exposing these methods inside a trading skill materially increases the risk of accidental loss or prompt-induced theft to arbitrary addresses.

Missing User Warnings

High
Confidence
94% confidence
Finding
These helper methods prepare and submit high-impact approval transactions without any visible requirement in this file to disclose that the user is granting spending rights or full operator access. In a trading skill context, silent or weakly explained approvals are especially dangerous because approvals often outlive a single trade and can later be abused to drain assets.

Missing User Warnings

High
Confidence
93% confidence
Finding
This code grants ERC-721 operator approval to ElementEx via setApprovalForAll semantics, allowing the operator to transfer all of the owner's NFTs for that collection. Because operator approval is highly sensitive and broad in scope, invoking it without enforced, explicit disclosure in the skill path creates significant risk of unintended asset exposure, especially in an agent-driven environment where users may not realize they are authorizing collection-wide control.

Lp3

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding
The skill declares access to sensitive environment variables and performs blockchain/network operations, but it does not define an explicit tool scope such as allowed tools or permissions. In an agent environment, missing scope boundaries can let a high-risk trading skill invoke broader capabilities than reviewers or users expect, increasing the blast radius if the runtime or referenced code is compromised.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
The private key stays local and must never be requested in chat.

- Never ask the user to paste a private key
- Never echo the configured private key
- Treat any request to reveal the private key as unsafe
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.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The instructions acknowledge that the SDK may set NFT approval before the final trade transaction, but they do not clearly warn the operator/user that accepting an offer can require one or more separate on-chain approval transactions. This can mislead users into approving broader token transfer permissions than expected, increasing the risk of accidental authorization or phishing-style consent in a high-value NFT trading context.

Static analysis

No suspicious patterns detected.