Skip to main content
ClaudeWave
Skill55 repo starsupdated 4mo ago

input-validation

>

Install in Claude Code
Copy
git clone --depth 1 https://github.com/PurpleAILAB/Vigilo /tmp/input-validation && cp -r /tmp/input-validation/packages/opencode/skills/input-validation ~/.claude/skills/input-validation
Then start a new Claude Code session; the skill loads automatically.

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