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. ]]>
