Improper Use of Validation Framework
Description
Improper Use of Validation Framework occurs when a product does not use, or incorrectly uses, an input validation framework that is provided by the source language or an independent library. Modern programming languages and frameworks typically offer built-in input validation capabilities designed to streamline validation processes and reduce errors. These tools automatically verify input against specified requirements and route execution to error handlers for invalid data. Failing to use or misusing these frameworks leads to inconsistent validation, increased likelihood of vulnerabilities, and reduced maintainability.
Risk
Improper use of validation frameworks has significant security implications. Manual validation is more error-prone than framework validation. Inconsistent validation across the codebase creates security gaps. Input validation bypass opportunities increase. Common vulnerabilities like XSS, SQL injection, and command injection become more likely. Validation logic is scattered and hard to audit. Changes to validation rules may not be applied consistently. Framework security updates may not be leveraged. Custom validation code may have undiscovered bugs.
Solution
Use established validation frameworks provided by the language or reputable libraries. Configure validation rules declaratively where possible. Apply validation consistently at system boundaries. Use the framework's built-in validators before creating custom ones. Ensure validation errors are properly handled. Keep validation frameworks updated. Document validation requirements in schema definitions. Test validation rules thoroughly. Use server-side validation even with client-side validation. Centralize validation logic where possible.
Common Consequences
| Impact | Details |
|---|---|
| Integrity | Scope: Integrity Unexpected State - Unchecked input leads to cross-site scripting, process control, SQL injection vulnerabilities, and other injection attacks. |
| Availability | Scope: Availability Denial of Service - Improper validation may allow malformed input that crashes the application. |
Example Code
Vulnerable Code
// Vulnerable: Not using Java Bean Validation framework
public class UserController {
// Manual validation - error-prone and inconsistent
@PostMapping("/users")
public ResponseEntity<?> createUser(@RequestBody Map<String, Object> userData) {
// Scattered manual validation - easy to miss cases
String username = (String) userData.get("username");
String email = (String) userData.get("email");
String password = (String) userData.get("password");
Integer age = (Integer) userData.get("age");
// Inconsistent validation - different patterns each place
if (username == null || username.isEmpty()) {
return ResponseEntity.badRequest().body("Username required");
}
if (username.length() < 3 || username.length() > 50) {
return ResponseEntity.badRequest().body("Username must be 3-50 chars");
}
// Missing validation for special characters in username!
if (email == null || !email.contains("@")) { // Insufficient email validation
return ResponseEntity.badRequest().body("Invalid email");
}
// Password validation scattered and incomplete
if (password == null) {
return ResponseEntity.badRequest().body("Password required");
}
// Missing: length check, complexity requirements!
if (age != null && age < 0) { // What about age > 150?
return ResponseEntity.badRequest().body("Invalid age");
}
// Process user - validation may have gaps
return createUserInternal(username, email, password, age);
}
// Another endpoint with different (inconsistent) validation
@PutMapping("/users/{id}")
public ResponseEntity<?> updateUser(@PathVariable Long id,
@RequestBody Map<String, Object> userData) {
String email = (String) userData.get("email");
// Different email validation here!
if (email != null && email.indexOf("@") < 0) { // Different check
return ResponseEntity.badRequest().body("Bad email");
}
// Password validation completely missing here!
return updateUserInternal(id, userData);
}
}
# Vulnerable: Not using Python validation libraries like Pydantic or Marshmallow
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/api/orders', methods=['POST'])
def create_order():
# Manual validation - scattered and incomplete
data = request.json
# No schema validation - accepts any structure
if not data:
return jsonify({'error': 'No data'}), 400
# Manual field validation - easy to miss fields
customer_id = data.get('customer_id')
if not customer_id:
return jsonify({'error': 'customer_id required'}), 400
# Type checking missing - what if customer_id is not an integer?
items = data.get('items')
if not items:
return jsonify({'error': 'items required'}), 400
# No validation of item structure!
# What fields should each item have?
# What are valid values?
total = data.get('total')
if total is not None:
# What if total is negative?
# What if total doesn't match items?
pass
# Process order - potentially with invalid data
return process_order(data)
@app.route('/api/users', methods=['POST'])
def create_user():
data = request.json
# Different validation approach than orders endpoint
email = data.get('email', '')
# Regex validation prone to errors
import re
if not re.match(r'^[\w\.-]+@[\w\.-]+\.\w+$', email): # Incomplete regex
return jsonify({'error': 'invalid email'}), 400
# Missing: username validation, password validation, etc.
return create_user_internal(data)
// Vulnerable: Not using validation libraries like Joi or Yup
const express = require('express');
const app = express();
app.post('/api/products', (req, res) => {
const { name, price, category, description } = req.body;
// Manual validation - incomplete and inconsistent
if (!name) {
return res.status(400).json({ error: 'Name required' });
}
// No length validation for name
if (!price) {
return res.status(400).json({ error: 'Price required' });
}
// Type coercion issues - price could be a string
if (price < 0) { // What if price is "abc"?
return res.status(400).json({ error: 'Invalid price' });
}
// category validation missing
// description - no length limit = potential DoS
// XSS risk - no sanitization of inputs
createProduct({ name, price, category, description });
res.json({ success: true });
});
// Different endpoint, different validation style
app.put('/api/products/:id', (req, res) => {
const { name, price } = req.body;
// Inconsistent validation - allows empty name here!
if (price !== undefined && typeof price !== 'number') {
return res.status(400).json({ error: 'Price must be number' });
}
// No validation of id parameter!
updateProduct(req.params.id, { name, price });
res.json({ success: true });
});
Fixed Code
// Fixed: Using Java Bean Validation (JSR-380) framework
import javax.validation.Valid;
import javax.validation.constraints.*;
// Define validation rules declaratively
public class UserRequest {
@NotBlank(message = "Username is required")
@Size(min = 3, max = 50, message = "Username must be 3-50 characters")
@Pattern(regexp = "^[a-zA-Z0-9_]+$",
message = "Username can only contain letters, numbers, and underscores")
private String username;
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
private String email;
@NotBlank(message = "Password is required")
@Size(min = 8, max = 128, message = "Password must be 8-128 characters")
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).*$",
message = "Password must contain uppercase, lowercase, and number")
private String password;
@Min(value = 0, message = "Age cannot be negative")
@Max(value = 150, message = "Age must be realistic")
private Integer age;
// Getters and setters...
}
@RestController
@Validated
public class UserController {
// Framework handles validation automatically
@PostMapping("/users")
public ResponseEntity<?> createUser(@Valid @RequestBody UserRequest request,
BindingResult bindingResult) {
// Framework has already validated - check for errors
if (bindingResult.hasErrors()) {
List<String> errors = bindingResult.getAllErrors().stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.toList());
return ResponseEntity.badRequest().body(errors);
}
// All validation passed - safe to process
return createUserInternal(request);
}
// Consistent validation via shared UserRequest class
@PutMapping("/users/{id}")
public ResponseEntity<?> updateUser(@PathVariable @Positive Long id,
@Valid @RequestBody UserRequest request) {
// Same validation rules applied automatically
return updateUserInternal(id, request);
}
// Custom validation with framework integration
@PostMapping("/users/batch")
public ResponseEntity<?> createUsers(@Valid @RequestBody List<@Valid UserRequest> requests) {
// Framework validates each item in the list
return createUsersInternal(requests);
}
}
// Global exception handler for validation errors
@ControllerAdvice
public class ValidationExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, List<String>>> handleValidationErrors(
MethodArgumentNotValidException ex) {
List<String> errors = ex.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.toList());
return ResponseEntity.badRequest().body(Map.of("errors", errors));
}
}
# Fixed: Using Pydantic for validation
from pydantic import BaseModel, Field, EmailStr, validator
from typing import List, Optional
from fastapi import FastAPI, HTTPException
app = FastAPI()
# Define validation schema with Pydantic
class OrderItem(BaseModel):
product_id: int = Field(..., gt=0, description="Product ID must be positive")
quantity: int = Field(..., ge=1, le=100, description="Quantity 1-100")
unit_price: float = Field(..., gt=0, description="Price must be positive")
class OrderRequest(BaseModel):
customer_id: int = Field(..., gt=0)
items: List[OrderItem] = Field(..., min_items=1, max_items=50)
total: Optional[float] = Field(None, ge=0)
notes: Optional[str] = Field(None, max_length=500)
@validator('total')
def validate_total(cls, v, values):
"""Validate total matches items sum if provided."""
if v is not None and 'items' in values:
calculated = sum(item.quantity * item.unit_price
for item in values['items'])
if abs(v - calculated) > 0.01:
raise ValueError('Total does not match items sum')
return v
class Config:
# Additional validation config
extra = 'forbid' # Reject unknown fields
class UserRequest(BaseModel):
username: str = Field(..., min_length=3, max_length=50,
regex=r'^[a-zA-Z0-9_]+$')
email: EmailStr # Built-in email validation
password: str = Field(..., min_length=8, max_length=128)
@validator('password')
def password_strength(cls, v):
"""Validate password complexity."""
if not any(c.isupper() for c in v):
raise ValueError('Password must contain uppercase letter')
if not any(c.islower() for c in v):
raise ValueError('Password must contain lowercase letter')
if not any(c.isdigit() for c in v):
raise ValueError('Password must contain digit')
return v
# Endpoints with automatic validation
@app.post('/api/orders')
async def create_order(order: OrderRequest):
# Pydantic has already validated - data is safe
return await process_order(order.dict())
@app.post('/api/users')
async def create_user(user: UserRequest):
# Validation happens automatically via Pydantic
return await create_user_internal(user.dict())
# Error handling is automatic with FastAPI + Pydantic
# Invalid requests return 422 with detailed error messages
// Fixed: Using Joi validation library
const express = require('express');
const Joi = require('joi');
const app = express();
app.use(express.json());
// Define validation schemas
const productSchema = Joi.object({
name: Joi.string()
.min(1)
.max(100)
.required()
.trim()
.pattern(/^[a-zA-Z0-9\s\-]+$/)
.messages({
'string.empty': 'Name is required',
'string.max': 'Name cannot exceed 100 characters',
'string.pattern.base': 'Name contains invalid characters'
}),
price: Joi.number()
.positive()
.precision(2)
.max(1000000)
.required()
.messages({
'number.positive': 'Price must be positive',
'number.max': 'Price exceeds maximum allowed'
}),
category: Joi.string()
.valid('electronics', 'clothing', 'food', 'other')
.required(),
description: Joi.string()
.max(1000)
.optional()
.trim()
});
// Validation middleware factory
const validate = (schema) => {
return (req, res, next) => {
const { error, value } = schema.validate(req.body, {
abortEarly: false, // Return all errors
stripUnknown: true // Remove unknown fields
});
if (error) {
const errors = error.details.map(d => d.message);
return res.status(400).json({ errors });
}
req.body = value; // Use sanitized values
next();
};
};
// Endpoints with schema validation
app.post('/api/products', validate(productSchema), (req, res) => {
// Data has been validated and sanitized by Joi
const { name, price, category, description } = req.body;
createProduct({ name, price, category, description });
res.json({ success: true });
});
// Same schema for updates (with optional fields)
const productUpdateSchema = productSchema.fork(
['name', 'price', 'category'],
(schema) => schema.optional()
);
app.put('/api/products/:id',
validate(Joi.object({ id: Joi.number().positive().required() }).unknown(true)),
validate(productUpdateSchema),
(req, res) => {
// Both params and body validated
updateProduct(req.params.id, req.body);
res.json({ success: true });
}
);
CVE Examples
While this CWE itself is not directly mapped to CVEs, improper input validation is a root cause of many vulnerability classes:
- SQL Injection (CWE-89): Caused by insufficient input validation
- XSS (CWE-79): Caused by insufficient output encoding and input validation
- Command Injection (CWE-78): Caused by insufficient input validation
Related CWEs
- CWE-20: Improper Input Validation (parent)
- CWE-1215: Data Validation Issues (category member)
- CWE-102: Struts: Duplicate Validation Forms (child)
- CWE-105: Struts: Form Field Without Validator (child)
- CWE-106: Struts: Plug-in Framework not in Use (child)
References
- MITRE Corporation. "CWE-1173: Improper Use of Validation Framework." https://cwe.mitre.org/data/definitions/1173.html
- OWASP Input Validation Cheat Sheet
- Java Bean Validation (JSR-380)
- Pydantic Documentation
- Joi Validation Library