T09 · Insecure Skill Coding Practices
- Location
- references/nullifiers.md:39
- Finding
- Random nullifier identifiers fail to enforce idempotency across repeated business operations<![CDATA[ ## Vulnerability Details **File Location**: `references/nullifiers.md:39-41` and `references/nullifiers.md:143-148` **Vulnerability Type**: `T09: Insecure Skill Coding Practices` **Risk Level**: Medium ### Vulnerable Code At `references/nullifiers.md:39-41`: ```typescript // Create a unique 32-byte ID (e.g., hash of payment inputs) const id = new Uint8Array(32); crypto.getRandomValues(id); ``` At `references/nullifiers.md:143-148`: ```typescript async function main() { // Generate random 32-byte identifier const id = new Uint8Array(crypto.randomBytes(32)); // Build nullifier instruction const ix = await createNullifierIx(rpc, payer.publicKey, id); ``` ### Technical Analysis A nullifier prevents reuse only when every execution of the same logical operation derives or retrieves the same identifier. The examples generate a new random identifier for each invocation. Consequently, retrying an identical payment after a timeout, process restart, concurrent request, or user resubmission creates a different PDA and does not conflict with the original nullifier. The documented duplicate rejection works only if the same in-memory `id` value is deliberately reused. Randomness ensures uniqueness between invocations, but it does not bind the nullifier to the payment or business operation whose duplicate execution must be prevented. A secure design should derive the nullifier from an immutable, canonical operation identifier, or persist a randomly generated operation ID before the transaction is built and reuse it for every retry. ### Attack Path 1. A payment endpoint or worker creates a random nullifier ID. 2. It submits a transaction containing the nullifier and payment instructions. 3. The payment executes, but the caller receives a timeout or otherwise requests a retry. 4. The application invokes the example flow again and generates a new random ID. 5. The new ID derives a different nullifier PDA, so the on-chain duplicate check succeeds. 6. T ...[truncated 817 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Define a canonical operation ID before constructing the transaction, such as a database payment ID, invoice ID, or client idempotency key. 2. Derive the nullifier deterministically with explicit domain separation: ```typescript const canonicalInput = [ "my-application", "production", "payment-v1", paymentId, ].join(":"); const id = new Uint8Array( crypto.createHash("sha256").update(canonicalInput).digest() ); ``` 3. If a random operation ID is required, generate it once, persist it atomically with the pending payment record, and reuse it for all retries. 4. Do not derive the ID from ambiguous string concatenation. Use canonical serialization with fixed field ordering and explicit type or length boundaries. 5. Include application, environment, network, and operation-type domains so unrelated actions cannot collide. 6. Enforce a unique constraint on the operation or idempotency key in application storage before transaction submission. 7. Add tests covering concurrent duplicate requests, retries after timeouts, service restarts, and retries after successful on-chain execution. 8. Update the examples to distinguish a unique random nullifier from a deterministic idempotency nullifier and warn that generating a fresh ID on every retry does not prevent duplicate business actions. ]]>
