T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/post-moment-ai.js:159
- Finding
- Opaque Server-Generated Blockchain Transactions Are Signed Without Local Validation<![CDATA[ ## Vulnerability Details **File Locations**: - `scripts/post-moment-ai.js:159-230` and `scripts/post-moment-ai.js:337-339` - `scripts/post-moment.js:39-57` and `scripts/post-moment.js:84-86` - `scripts/post-moment-with-image.js:75-95` and `scripts/post-moment-with-image.js:150-152` - `scripts/mint-likes.js:34-54` and `scripts/mint-likes.js:66-68` **Vulnerability Type**: Signing and execution of unvalidated remote transaction payloads **Risk Level**: High ### Vulnerable Code The AI posting workflow signs a digest and uses transaction parameters supplied by the remote Forever Moments API: ```javascript async function relayExecute(payload, description) { console.log(`\n📡 ${description}`); const relayPrepare = await apiCall('/relay/prepare', 'POST', { upAddress: MY_UP, controllerAddress: CONTROLLER, payload: payload }); if (!relayPrepare.success) { console.error('❌ Relay prepare failed:', relayPrepare.error); return null; } const wallet = new ethers.Wallet(PRIVATE_KEY); const signature = wallet.signingKey.sign(ethers.getBytes(relayPrepare.data.hashToSign)); const relaySubmit = await apiCall('/relay/submit', 'POST', { upAddress: MY_UP, payload: payload, signature: signature.serialized, nonce: relayPrepare.data.lsp15Request.transaction.nonce, validityTimestamps: relayPrepare.data.lsp15Request.transaction.validityTimestamps, relayerUrl: relayPrepare.data.relayerUrl }); if (relaySubmit?.success && !relaySubmit.data?.ok) { let responseText = relaySubmit.data?.responseText || ''; if (typeof responseText === 'string' && responseText.includes('Insufficient balance')) { console.log('⚠️ Relayer quota exhausted. Falling back to direct execution (paying gas from controller)...'); return await directExecute(relayPrepare.data.keyManagerAddress, payload); } } return relaySubmit; } ``` Its direct-execution fallback also trusts the KeyManager destination ret ...[truncated 4689 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. **Decode and validate every payload locally** - Decode the outer Universal Profile and KeyManager calls. - Decode nested calls and verify contract destinations, function selectors, recipients, asset amounts, and native values. - Maintain a strict allowlist for supported Forever Moments contracts and operations. - Reject unknown selectors, multicalls, delegate calls, and unexpected nested payloads. 2. **Reconstruct the relay digest locally** - Derive the LSP25 digest from the verified payload, Universal Profile address, nonce, validity timestamps, chain ID, and applicable domain fields. - Compare the locally derived digest against `hashToSign`. - Refuse to sign if any field differs. 3. **Verify infrastructure destinations** - Resolve the expected KeyManager address from trusted on-chain Universal Profile state instead of accepting it from the API. - Restrict `relayerUrl` to an explicit allowlist such as the documented HTTPS LUKSO mainnet relayer. - Reject redirects to unapproved hosts. 4. **Protect value-bearing operations** - Parse and display the exact LYX/token amount, recipient, contract, and estimated gas before signing. - Require explicit user confirmation for LIKES minting and direct gas fallback. - Do not trigger direct execution merely from a server-controlled error string. 5. **Apply least privilege** - Use a dedicated controller for this Skill. - Grant only the exact CALL and EXECUTE_RELAY_CALL permissions needed for known Forever Moments contracts. - Avoid broad `SUPER_CALL`, unrestricted value transfer, ownership, and permission-management capabilities. 6. **Fail closed** - Treat malformed API responses, unknown contracts, digest mismatches, and validation failures as fatal. - Do not silently continue or downgrade to direct execution. ]]>
