Install in Claude Code
Copygit clone --depth 1 https://github.com/PurpleAILAB/Vigilo /tmp/input-validation && cp -r /tmp/input-validation/packages/opencode/skills/input-validation ~/.claude/skills/input-validationThen start a new Claude Code session; the skill loads automatically.
Definition
SKILL.md
# Input Validation Vulnerability Analysis
**OWASP SC04:2025** - Lack of Input Validation allows attackers to pass unexpected values that break contract invariants or enable exploits.
**2025-2026 Statistics**: Input validation failures account for **34.6% of all smart contract vulnerabilities**, contributing to $420M+ in losses.
---
## Why Input Validation Fails (Root Causes)
### Root Cause 1: Assumption of Good Faith
Developers assume callers will provide sensible inputs.
```solidity
// VULNERABLE: Assumes recipient is valid
function transfer(address to, uint256 amount) external {
balances[msg.sender] -= amount;
balances[to] += amount; // @audit to = address(0) burns tokens
}
```
**Attacker's view**: "They didn't check my input. Let me see what happens with edge cases."
### Root Cause 2: Missing Bounds Checking
Parameters have implicit bounds that aren't enforced.
```solidity
// VULNERABLE: No bounds on fee
function setFee(uint256 newFee) external onlyOwner {
fee = newFee; // @audit newFee = 100% drains users
}
```
### Root Cause 3: Type Confusion
Solidity's loose typing allows unexpected conversions.
```solidity
// VULNERABLE: Assumes bytes4 is valid selector
function executeCall(address target, bytes4 selector, bytes calldata data) external {
(bool success,) = target.call(abi.encodePacked(selector, data));
// @audit selector could be anything, including dangerous functions
}
```
### Root Cause 4: Array/Calldata Trust
Trusting array lengths and calldata structure from external sources.
```solidity
// VULNERABLE: Trusts array lengths match
function batchTransfer(address[] calldata recipients, uint256[] calldata amounts) external {
for (uint256 i = 0; i < recipients.length; i++) {
_transfer(recipients[i], amounts[i]); // @audit Arrays may have different lengths
}
}
```
---
## The Input Validation Matrix (Core Artifact)
For each external function, document:
| Function | Parameter | Expected Range | Actual Check | Gap |
|----------|-----------|----------------|--------------|-----|
| transfer | to | != address(0) | None | **YES** |
| transfer | amount | <= balance | require(balance >= amount) | No |
| setFee | newFee | 0-1000 (0-10%) | None | **YES** |
| batchTransfer | recipients | len > 0 | None | **YES** |
| batchTransfer | amounts | len == recipients.len | None | **YES** |
---
## Detection Patterns
### Pattern 1: Missing Zero Address Check
**Root Cause**: Assumption of Good Faith
```solidity
// VULNERABLE: Zero address burns tokens/ETH
function withdraw(address recipient) external {
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0;
payable(recipient).transfer(amount); // @audit recipient = 0x0 burns ETH
}
// VULNERABLE: Zero address as critical role
function setAdmin(address newAdmin) external onlyOwner {
admin = newAdmin; // @audit newAdmin = 0x0 bricks admin functions
}
```
**Attack Flow**:
1. Attacker (or accident) calls with address(0)
2. Tokens/ETH sent to zero address are burned
3. Or critical role set to zero, bricking functionality
4. Irreversible loss
**Search Queries**:
```
Grep("address.*=|= address", glob="**/*.sol")
Grep("require.*!= address\\(0\\)", glob="**/*.sol")
Grep("setAdmin|setOwner|set.*Address", glob="**/*.sol")
```
**Mitigation**:
```solidity
function setAdmin(address newAdmin) external onlyOwner {
require(newAdmin != address(0), "Zero address");
admin = newAdmin;
}
```
### Pattern 2: Missing Amount/Value Bounds
**Root Cause**: Missing Bounds Checking
```solidity
// VULNERABLE: Fee can be set to 100%
function setFee(uint256 newFee) external onlyOwner {
fee = newFee; // @audit No maximum check
}
function withdraw(uint256 amount) external {
uint256 feeAmount = amount * fee / 10000;
uint256 netAmount = amount - feeAmount; // @audit Can underflow if fee > 10000
token.transfer(msg.sender, netAmount);
}
```
**Attack Flow**:
1. Malicious/compromised admin sets fee = 10000 (100%)
2. All user withdrawals get 0 tokens
3. Or fee = 20000 causes underflow (reverts all withdrawals)
**Search Queries**:
```
Grep("setFee|setRate|setMultiplier|setPercent", glob="**/*.sol")
Grep("external.*uint.*\\{[^}]*=[^}]*\\}", glob="**/*.sol")
```
**Mitigation**:
```solidity
uint256 public constant MAX_FEE = 1000; // 10%
function setFee(uint256 newFee) external onlyOwner {
require(newFee <= MAX_FEE, "Fee too high");
fee = newFee;
}
```
### Pattern 3: Array Length Mismatch
**Root Cause**: Array/Calldata Trust
```solidity
// VULNERABLE: No length check
function batchTransfer(
address[] calldata recipients,
uint256[] calldata amounts
) external {
for (uint256 i = 0; i < recipients.length; i++) {
_transfer(recipients[i], amounts[i]); // @audit Out of bounds if amounts shorter
}
}
```
**Attack Flow**:
1. Attacker calls with recipients.length = 10, amounts.length = 5
2. Loop accesses amounts[5], amounts[6], etc.
3. Reverts or reads garbage data
4. Unexpected behavior
**Search Queries**:
```
Grep("\\[\\].*calldata.*,.*\\[\\].*calldata", glob="**/*.sol")
Grep("for.*recipients\\.length|for.*addresses\\.length", glob="**/*.sol")
```
**Mitigation**:
```solidity
function batchTransfer(
address[] calldata recipients,
uint256[] calldata amounts
) external {
require(recipients.length == amounts.length, "Length mismatch");
require(recipients.length > 0, "Empty array");
require(recipients.length <= MAX_BATCH_SIZE, "Batch too large");
// ...
}
```
### Pattern 4: Missing Zero Amount Check
**Root Cause**: Assumption of Good Faith
```solidity
// VULNERABLE: Zero amount wastes gas and may break logic
function stake(uint256 amount) external {
token.transferFrom(msg.sender, address(this), amount);
stakes[msg.sender] += amount;
emit Staked(msg.sender, amount);
// @audit amount = 0 creates empty stake event, may break off-chain tracking
}
// VULNERABLE: Zero amount mints shares
function deposit(uint256