T09 · Insecure Skill Coding Practices
- Location
programs-anchor.md:235- Finding
Unchecked Batch Length Causes an Out-of-Bounds Panic
- Content
View full analysis
, amounts: Vec) -> Result<()> { let remaining = &ctx.remaining_accounts; require!(remaining.len() % 2 == 0, BatchError::InvalidSchema); for (i, chunk) in remaining.chunks(2).enumerate() { process_pair(&chunk[0], &chunk[1], amounts[i])?; } Ok(()) } ``` ### Technical Analysis The example verifies that the number of remaining accounts is even, but it does not verify that `amounts` contains one element for every account pair. Both the remaining accounts and instruction arguments are attacker-controlled. If the number of account pairs exceeds `amounts.len()`, the expression `amounts[i]` performs an out-of-bounds vector access and panics. This produces an uncontrolled program failure instead of a defined `ProgramError`. The required invariant is: ```text amounts.len() == remaining.len() / 2 ``` Although Solana transaction execution is atomic, panic-based input handling is unsafe and can consume compute before aborting. Copying this documentation pattern into production batch processors can also obscure malformed input handling and create repeatable denial-of-service behavior for transaction relayers or services that submit attacker-provided batches. ### Attack Path 1. An attacker constructs a call to the documented `batch_operation` instruction. 2. The attacker supplies an even number of remaining accounts, such as four accounts. 3. The attacker supplies fewer amount values than account pairs, such as an empty vector or one amount. 4. The modulo validation succeeds because four is even. 5. Iteration reaches an account pair for which no corresponding amount exists. 6. `amounts[i]` indexes outside the vector and panics. 7. The transaction aborts after consuming processing ...[truncated 490 chars]- Remediation
View remediation
, amounts: Vec) -> Result<()> { let remaining = &ctx.remaining_accounts; require!( remaining.len() % 2 == 0, BatchError::InvalidSchema ); require!( amounts.len() == remaining.len() / 2, BatchError::InvalidSchema ); for (chunk, amount) in remaining.chunks_exact(2).zip(amounts.iter()) { process_pair(&chunk[0], &chunk[1], *amount)?; } Ok(()) } ``` Additional hardening should include: - Define a dedicated error for mismatched amount and account counts. - Use `chunks_exact(2)` to encode the pairing requirement explicitly. - Avoid direct indexing into attacker-controlled vectors where iterator-based access is possible. - Set a maximum batch size to constrain compute consumption. - Add tests for empty input, odd account counts, too few amounts, too many amounts, and maximum permitted batch size. ]]>
