# Input Validation Security Audit (CWE-20)

> Covers OWASP A03 | Improper Input Validation | Type Confusion | Boundary Checks | Format Validation
>
> Universal rules applicable to all languages

---

## Table of Contents

1. [CWE-20 Overview](#1-cwe-20-overview)
2. [Missing Input Validation Patterns](#2-missing-input-validation-patterns)
3. [Type Validation](#3-type-validation)
4. [Boundary Checks](#4-boundary-checks)
5. [Format Validation](#5-format-validation)
6. [Multi-Language Detection Rules](#6-multi-language-detection-rules)
7. [Secure Validation Best Practices](#7-secure-validation-best-practices)

---

## 1. CWE-20 Overview

### 1.1 Risk Definition

**CWE-20: Improper Input Validation** is the root cause of all injection-type vulnerabilities and ranks 6th in the CWE Top 25.

| Risk | Description |
|------|-------------|
| **Direct Impact** | SQL injection, XSS, command injection, path traversal |
| **Indirect Impact** | Business logic bypass, DoS, data corruption |
| **CVSS** | Depends on downstream vulnerability, can reach 9.8 |

### 1.2 Validation Layers

```
┌─────────────────────────────────────────────┐
│  Client-Side Validation (UX, Not Trusted)     │
├─────────────────────────────────────────────┤
│  API Gateway/WAF Validation (Edge, Coarse)   │
├─────────────────────────────────────────────┤
│  Application-Layer Validation (Core, Required)│  ← Audit Focus
│  - Type validation                            │
│  - Boundary checks                            │
│  - Format validation                          │
│  - Business rule validation                   │
├─────────────────────────────────────────────┤
│  Data-Layer Validation (Fallback, Constraints)│
└─────────────────────────────────────────────┘
```

---

## 2. Missing Input Validation Patterns

### 2.1 Direct Use of Unvalidated Input

```java
// 🔴 Java - Direct use of request parameters
@GetMapping("/search")
public List<Product> search(@RequestParam String keyword) {
    return productRepository.findByNameContaining(keyword);  // No length/character limits
}

// 🔴 Direct use in file operations
@GetMapping("/download")
public void download(@RequestParam String filename, HttpServletResponse response) {
    File file = new File("/uploads/" + filename);  // Path traversal
    // ...
}
```

```python
# 🔴 Python - Unvalidated parameters
@app.get("/users/{user_id}")
async def get_user(user_id):  # Type not specified
    return await User.get(id=int(user_id))  # int() may throw exception

# 🔴 Direct string concatenation
@app.get("/files")
async def get_file(path: str):  # No format validation
    return FileResponse(f"/data/{path}")
```

```javascript
// 🔴 Node.js - No validation
app.get('/api/data', (req, res) => {
    const { page, limit } = req.query;  // Types not validated
    const offset = page * limit;  // NaN risk
    // ...
});

// 🔴 Direct use
app.post('/api/execute', (req, res) => {
    const { command } = req.body;  // No whitelist
    exec(command);  // Command injection
});
```

```go
// 🔴 Go - Type conversion without error handling
func handler(w http.ResponseWriter, r *http.Request) {
    id := r.URL.Query().Get("id")
    num, _ := strconv.Atoi(id)  // 🔴 Error ignored
    // ...
}
```

### 2.2 Validation Bypass

```java
// 🔴 Client-side validation only
// Frontend: if (age < 0 || age > 150) return false;
// Backend has no validation, attacker can bypass by sending requests directly

// 🔴 Blacklist validation (bypassable)
public boolean isValidFilename(String filename) {
    String[] blacklist = {"..", "/", "\\"};
    for (String bad : blacklist) {
        if (filename.contains(bad)) return false;
    }
    return true;  // 🔴 Bypassable with URL encoding: %2e%2e%2f
}

// 🔴 Overly permissive regex
Pattern.matches("[a-zA-Z0-9]+", input);  // 🔴 Allows empty string
Pattern.matches(".*@.*\\..*", email);    // 🔴 Matches @.
```

---

## 3. Type Validation

### 3.1 Numeric Types

```java
// 🔴 Integer overflow
public void transfer(long amount) {
    if (amount <= 0) throw new IllegalArgumentException();
    account.balance -= amount;  // 🔴 Upper bound not checked, possible overflow
}

// 🟢 Secure: Complete boundary check
public void transfer(long amount) {
    if (amount <= 0 || amount > MAX_TRANSFER_AMOUNT) {
        throw new IllegalArgumentException("Invalid amount");
    }
    if (account.balance < amount) {
        throw new InsufficientFundsException();
    }
    account.balance -= amount;
}
```

```python
# 🔴 Type confusion
def calculate_discount(price, discount):
    return price * (1 - discount)  # discount could be string "0.5"

# 🟢 Secure: Type enforcement
def calculate_discount(price: Decimal, discount: Decimal) -> Decimal:
    if not (0 <= discount <= 1):
        raise ValueError("Discount must be between 0 and 1")
    return price * (Decimal('1') - discount)
```

```javascript
// 🔴 JavaScript type traps
function processAge(age) {
    if (age > 0) {  // "10" > 0 is true (string comparison)
        return age + 1;  // "10" + 1 = "101"
    }
}

// 🟢 Secure: Type conversion and validation
function processAge(age) {
    const numAge = Number(age);
    if (!Number.isInteger(numAge) || numAge < 0 || numAge > 150) {
        throw new Error('Invalid age');
    }
    return numAge + 1;
}
```

### 3.2 String Types

```java
// 🔴 Empty string/null not handled
public User findUser(String username) {
    return userRepository.findByUsername(username);  // null/empty string not checked
}

// 🟢 Secure
public User findUser(String username) {
    if (username == null || username.isBlank()) {
        throw new IllegalArgumentException("Username required");
    }
    if (username.length() > 50) {
        throw new IllegalArgumentException("Username too long");
    }
    return userRepository.findByUsername(username.trim());
}
```

### 3.3 Array/Collection Types

```java
// 🔴 Array index not validated
public String getItem(String[] items, int index) {
    return items[index];  // ArrayIndexOutOfBoundsException
}

// 🔴 Batch operations without limits
@PostMapping("/batch-delete")
public void batchDelete(@RequestBody List<Long> ids) {
    itemRepository.deleteAllByIdIn(ids);  // 🔴 No quantity limit, DoS possible
}

// 🟢 Secure
@PostMapping("/batch-delete")
public void batchDelete(@RequestBody @Size(max = 100) List<Long> ids) {
    if (ids == null || ids.isEmpty()) {
        throw new IllegalArgumentException("IDs required");
    }
    itemRepository.deleteAllByIdIn(ids);
}
```

---

## 4. Boundary Checks

### 4.1 Numeric Boundaries

```java
// 🔴 Pagination parameters without bounds
@GetMapping("/list")
public Page<Item> list(@RequestParam int page, @RequestParam int size) {
    return itemRepository.findAll(PageRequest.of(page, size));  // size=1000000 -> OOM
}

// 🟢 Secure
@GetMapping("/list")
public Page<Item> list(
    @RequestParam @Min(0) int page,
    @RequestParam @Min(1) @Max(100) int size) {
    return itemRepository.findAll(PageRequest.of(page, Math.min(size, 100)));
}
```

```python
# 🔴 Amount boundary
async def create_order(amount: float):
    order = Order(amount=amount)  # Negative? Extremely large?

# 🟢 Secure
async def create_order(amount: Decimal):
    if amount <= 0:
        raise ValueError("Amount must be positive")
    if amount > Decimal('1000000'):
        raise ValueError("Amount exceeds limit")
    order = Order(amount=amount)
```

### 4.2 Length Boundaries

```java
// 🔴 String length without limit
@PostMapping("/comment")
public void addComment(@RequestBody String content) {
    commentRepository.save(new Comment(content));  // 10MB content?
}

// 🟢 Secure
@PostMapping("/comment")
public void addComment(@RequestBody @Size(min = 1, max = 5000) String content) {
    commentRepository.save(new Comment(content.trim()));
}
```

### 4.3 Time Boundaries

```java
// 🔴 Date range without limit
@GetMapping("/report")
public Report getReport(@RequestParam LocalDate start, @RequestParam LocalDate end) {
    return reportService.generate(start, end);  // 10-year range -> timeout
}

// 🟢 Secure
@GetMapping("/report")
public Report getReport(@RequestParam LocalDate start, @RequestParam LocalDate end) {
    if (start.isAfter(end)) {
        throw new IllegalArgumentException("Invalid date range");
    }
    if (ChronoUnit.DAYS.between(start, end) > 365) {
        throw new IllegalArgumentException("Range exceeds 1 year");
    }
    return reportService.generate(start, end);
}
```

---

## 5. Format Validation

### 5.1 Email Format

```java
// 🔴 Weak regex
Pattern.matches(".*@.*", email);  // Matches "@"

// 🔴 Overly complex regex (ReDoS risk)
Pattern.matches("^([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,5})$", email);

// 🟢 Secure: Use a validation library
import org.apache.commons.validator.routines.EmailValidator;
EmailValidator.getInstance().isValid(email);

// Or Bean Validation
@Email
private String email;
```

### 5.2 URL Format

```java
// 🔴 Prefix check only
if (url.startsWith("http://") || url.startsWith("https://")) {
    fetch(url);  // Could be http://internal-server
}

// 🟢 Secure: URL parsing + whitelist
try {
    URL parsed = new URL(url);
    if (!ALLOWED_HOSTS.contains(parsed.getHost())) {
        throw new SecurityException("Host not allowed");
    }
    if (!"https".equals(parsed.getProtocol())) {
        throw new SecurityException("HTTPS required");
    }
    fetch(url);
} catch (MalformedURLException e) {
    throw new IllegalArgumentException("Invalid URL");
}
```

### 5.3 Filename Format

```java
// 🔴 Allows path separators
public void saveFile(String filename, byte[] content) {
    Files.write(Paths.get("/uploads/" + filename), content);  // ../../../etc/passwd
}

// 🟢 Secure
public void saveFile(String filename, byte[] content) {
    // Only allow alphanumeric characters and dots
    if (!filename.matches("^[a-zA-Z0-9][a-zA-Z0-9._-]{0,100}$")) {
        throw new IllegalArgumentException("Invalid filename");
    }
    // Disallow dangerous extensions
    String ext = FilenameUtils.getExtension(filename).toLowerCase();
    if (DANGEROUS_EXTENSIONS.contains(ext)) {
        throw new SecurityException("File type not allowed");
    }
    Path path = Paths.get("/uploads").resolve(filename).normalize();
    if (!path.startsWith("/uploads")) {
        throw new SecurityException("Path traversal detected");
    }
    Files.write(path, content);
}
```

### 5.4 JSON Format

```java
// 🔴 Deep nesting DoS
ObjectMapper mapper = new ObjectMapper();
JsonNode node = mapper.readTree(jsonInput);  // {"a":{"a":{"a":...}}} 1000 levels

// 🟢 Secure: Limit depth
ObjectMapper mapper = new ObjectMapper();
mapper.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION);
mapper.getFactory().setStreamReadConstraints(
    StreamReadConstraints.builder()
        .maxNestingDepth(50)
        .maxStringLength(10_000_000)
        .build()
);
```

---

## 6. Multi-Language Detection Rules

### 6.1 Detection Commands

```bash
# Find unvalidated parameter usage
# Java
rg -n "@RequestParam\s+\w+\s+\w+[^@]" --glob "*.java" | grep -v "@Valid\|@NotNull\|@Size\|@Min\|@Max"

# Python/FastAPI
rg -n "def.*\(.*:.*\):" --glob "*.py" | grep -v "Annotated\|Query\|Path\|Body"

# Node.js
rg -n "req\.(body|query|params)\." --glob "*.{js,ts}" | grep -v "validate\|sanitize\|joi\|zod"

# Go
rg -n "r\.URL\.Query\(\)|r\.FormValue\(" --glob "*.go"

# Find potential boundary issues
rg -n "parseInt|parseFloat|Number\(|int\(|float\(|strconv\.Atoi" --glob "*.{js,ts,py,go}"

# Find unhandled errors
rg -n ", _\s*:?=|, err\s*:?=.*\n\s*[^if]" --glob "*.go"
```

### 6.2 Framework Validation Annotations/Decorators

| Language/Framework | Validation Method | Example |
|-----------|----------|------|
| Java/Spring | Bean Validation | `@Valid @NotNull @Size @Min @Max @Pattern @Email` |
| Python/Pydantic | Type annotations | `Field(min_length=1, max_length=100)` |
| Python/FastAPI | Query/Path | `Query(min_length=1, regex="^[a-z]+$")` |
| Node.js/Joi | Schema | `Joi.string().min(1).max(100).email()` |
| Node.js/Zod | Schema | `z.string().min(1).max(100).email()` |
| Go/validator | Struct tags | `` `validate:"required,min=1,max=100,email"` `` |
| .NET | DataAnnotations | `[Required] [StringLength(100)] [Range(0,100)]` |
| Ruby/Rails | ActiveModel | `validates :name, presence: true, length: { maximum: 100 }` |

---

## 7. Secure Validation Best Practices

### 7.1 Validation Strategy

```
1. Whitelist over blacklist
2. Server-side validation is mandatory; client-side validation is optional
3. Use validated data immediately after validation to avoid TOCTOU
4. Use mature validation libraries; avoid custom implementations
5. Log validation failures (but do not log sensitive data)
```

### 7.2 Universal Validation Checklist

```markdown
## Input Validation Audit Checklist

### Type Validation
- [ ] Are numeric parameters typed
- [ ] Do string parameters have length limits
- [ ] Do array parameters have size limits
- [ ] Do date parameters have format and range limits

### Boundary Checks
- [ ] Do numeric values have min/max limits
- [ ] Do pagination parameters have upper bounds
- [ ] Do batch operations have quantity limits

### Format Validation
- [ ] Are email/URL/phone numbers validated with standard methods
- [ ] Are filenames filtered for path characters
- [ ] Are custom formats validated with regex

### Business Validation
- [ ] Are state transitions validated for preconditions
- [ ] Are amount calculations validated for precision and range
- [ ] Are reference relationships validated for existence
```

### 7.3 Framework Configuration Examples

```java
// Spring Boot global validation configuration
@Configuration
public class ValidationConfig {
    @Bean
    public Validator validator() {
        ValidatorFactory factory = Validation.byDefaultProvider()
            .configure()
            .messageInterpolator(new ParameterMessageInterpolator())
            .buildValidatorFactory();
        return factory.getValidator();
    }
}

// Controller usage
@PostMapping("/users")
public User createUser(@Valid @RequestBody UserRequest request) {
    return userService.create(request);
}
```

```python
# FastAPI/Pydantic validation
from pydantic import BaseModel, Field, validator

class UserRequest(BaseModel):
    username: str = Field(min_length=3, max_length=50, regex=r'^[a-zA-Z0-9_]+$')
    email: EmailStr
    age: int = Field(ge=0, le=150)

    @validator('username')
    def username_not_reserved(cls, v):
        if v.lower() in ['admin', 'root', 'system']:
            raise ValueError('Reserved username')
        return v
```

```typescript
// NestJS/class-validator
import { IsEmail, IsInt, IsString, Length, Min, Max } from 'class-validator';

class CreateUserDto {
    @IsString()
    @Length(3, 50)
    @Matches(/^[a-zA-Z0-9_]+$/)
    username: string;

    @IsEmail()
    email: string;

    @IsInt()
    @Min(0)
    @Max(150)
    age: number;
}
```
