ASP.NET Misconfiguration: Improper Model Validation
Description
ASP.NET Misconfiguration: Improper Model Validation occurs when an ASP.NET application does not use, or incorrectly uses, the model validation framework provided by the ASP.NET MVC or ASP.NET Core framework. ASP.NET provides robust built-in model validation through data annotations and the ModelState validation mechanism. When developers fail to implement these validation features or implement them incorrectly, user input may not be properly validated, leading to various security vulnerabilities including injection attacks.
Risk
Improper model validation in ASP.NET applications has significant security implications. Unchecked input can lead to SQL injection attacks. Cross-site scripting (XSS) vulnerabilities become more likely. Business logic can be bypassed through invalid input. Process control attacks may be possible. Mass assignment vulnerabilities can occur. Data integrity is compromised. Application state can become corrupt. Authorization checks may be circumvented.
Solution
Always use the ASP.NET model validation framework. Apply data annotation attributes to model properties ([Required], [StringLength], [Range], etc.). Check ModelState.IsValid before processing input. Implement custom validation attributes for complex rules. Use [ValidateAntiForgeryToken] for form submissions. Configure model binding to restrict allowed properties. Use [Bind] attribute or view models to prevent over-posting. Enable client-side validation for better UX. Implement global error handling for validation failures. Keep ASP.NET framework updated.
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. |
Example Code
Vulnerable Code
// Vulnerable: ASP.NET MVC without model validation
public class UserController : Controller
{
// Vulnerable: No model validation
[HttpPost]
public ActionResult Register(string username, string email, string password)
{
// Direct use of input without validation
// No length checks, format validation, or required field checks
var user = new User
{
Username = username, // Could be null, empty, or too long
Email = email, // Could be invalid email format
Password = password // Could be weak or empty
};
// SQL injection possible if not using parameterized queries
db.Users.Add(user);
db.SaveChanges();
return RedirectToAction("Success");
}
// Vulnerable: Model binding without validation check
[HttpPost]
public ActionResult UpdateProfile(UserProfile profile)
{
// ModelState.IsValid is not checked!
// Even if model has validation attributes, they're not enforced
db.UserProfiles.Update(profile);
db.SaveChanges();
return RedirectToAction("Profile");
}
// Vulnerable: Over-posting attack possible
[HttpPost]
public ActionResult EditUser(User user)
{
// User can POST additional fields like "IsAdmin = true"
// that will be bound to the model
db.Entry(user).State = EntityState.Modified;
db.SaveChanges(); // May update fields that shouldn't be user-editable
return RedirectToAction("Index");
}
}
// Model without validation attributes
public class User
{
public int Id { get; set; }
public string Username { get; set; } // No validation
public string Email { get; set; } // No validation
public string Password { get; set; } // No validation
public bool IsAdmin { get; set; } // Can be mass-assigned!
}
// Vulnerable: ASP.NET Core API without validation
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
// Vulnerable: Not using [ApiController] automatic validation
// or manually checking ModelState
[HttpPost]
public async Task<IActionResult> Create(ProductDto product)
{
// No validation - product could have invalid data
var entity = new Product
{
Name = product.Name, // Could be null
Price = product.Price, // Could be negative
Category = product.Category
};
_context.Products.Add(entity);
await _context.SaveChangesAsync();
return Ok(entity);
}
// Vulnerable: Custom model binder bypassing validation
[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, [ModelBinder(typeof(CustomBinder))] Product product)
{
// Custom binder may not trigger validation
_context.Entry(product).State = EntityState.Modified;
await _context.SaveChangesAsync();
return NoContent();
}
}
// DTO without validation
public class ProductDto
{
public string Name { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
}
Fixed Code
// Fixed: ASP.NET MVC with proper model validation
public class UserController : Controller
{
// Fixed: Using validated model
[HttpPost]
[ValidateAntiForgeryToken] // CSRF protection
public ActionResult Register(RegisterViewModel model)
{
// Check model validation
if (!ModelState.IsValid)
{
// Return view with validation errors
return View(model);
}
var user = new User
{
Username = model.Username,
Email = model.Email,
PasswordHash = HashPassword(model.Password)
};
db.Users.Add(user);
db.SaveChanges();
return RedirectToAction("Success");
}
// Fixed: ModelState validation enforced
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult UpdateProfile(UserProfileViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var profile = db.UserProfiles.Find(model.Id);
if (profile == null || profile.UserId != User.GetUserId())
{
return NotFound();
}
// Only update allowed fields
profile.DisplayName = model.DisplayName;
profile.Bio = model.Bio;
db.SaveChanges();
return RedirectToAction("Profile");
}
// Fixed: Using [Bind] to prevent over-posting
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult EditUser([Bind("Id,Username,Email")] UserEditViewModel model)
{
// Only Id, Username, Email can be bound - IsAdmin cannot
if (!ModelState.IsValid)
{
return View(model);
}
var user = db.Users.Find(model.Id);
if (user == null)
{
return NotFound();
}
// Update only specific fields
user.Username = model.Username;
user.Email = model.Email;
db.SaveChanges();
return RedirectToAction("Index");
}
}
// View Model with validation attributes
public class RegisterViewModel
{
[Required(ErrorMessage = "Username is required")]
[StringLength(50, MinimumLength = 3,
ErrorMessage = "Username must be 3-50 characters")]
[RegularExpression(@"^[a-zA-Z0-9_]+$",
ErrorMessage = "Username can only contain letters, numbers, and underscores")]
public string Username { get; set; }
[Required(ErrorMessage = "Email is required")]
[EmailAddress(ErrorMessage = "Invalid email format")]
public string Email { get; set; }
[Required(ErrorMessage = "Password is required")]
[StringLength(128, MinimumLength = 8,
ErrorMessage = "Password must be at least 8 characters")]
[DataType(DataType.Password)]
public string Password { get; set; }
[DataType(DataType.Password)]
[Compare("Password", ErrorMessage = "Passwords do not match")]
public string ConfirmPassword { get; set; }
}
// Edit View Model - no sensitive fields exposed
public class UserEditViewModel
{
public int Id { get; set; }
[Required]
[StringLength(50, MinimumLength = 3)]
public string Username { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
// Note: IsAdmin is NOT included - cannot be mass-assigned
}
// Fixed: ASP.NET Core API with validation
[ApiController] // Enables automatic model validation
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
// Fixed: [ApiController] automatically validates ModelState
// Returns 400 Bad Request if validation fails
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateProductDto product)
{
// With [ApiController], ModelState is automatically checked
// Invalid requests never reach this point
var entity = new Product
{
Name = product.Name,
Price = product.Price,
Category = product.Category
};
_context.Products.Add(entity);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetById), new { id = entity.Id }, entity);
}
// Fixed: Explicit validation for more control
[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, [FromBody] UpdateProductDto product)
{
// Additional custom validation
if (id != product.Id)
{
return BadRequest("ID mismatch");
}
var entity = await _context.Products.FindAsync(id);
if (entity == null)
{
return NotFound();
}
// Update only allowed fields from DTO
entity.Name = product.Name;
entity.Price = product.Price;
entity.Category = product.Category;
// Note: other fields like CreatedBy, CreatedDate are not updated
await _context.SaveChangesAsync();
return NoContent();
}
}
// DTO with validation attributes
public class CreateProductDto
{
[Required(ErrorMessage = "Name is required")]
[StringLength(100, MinimumLength = 1,
ErrorMessage = "Name must be 1-100 characters")]
public string Name { get; set; }
[Required]
[Range(0.01, 1000000, ErrorMessage = "Price must be between 0.01 and 1,000,000")]
public decimal Price { get; set; }
[Required]
[RegularExpression(@"^(Electronics|Clothing|Food|Other)$",
ErrorMessage = "Invalid category")]
public string Category { get; set; }
}
// Custom validation attribute for complex rules
public class ValidCategoryAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext context)
{
var category = value as string;
var dto = context.ObjectInstance as CreateProductDto;
// Example: Food items must have price under 100
if (category == "Food" && dto?.Price > 100)
{
return new ValidationResult("Food items cannot cost more than $100");
}
return ValidationResult.Success;
}
}
CVE Examples
This CWE represents a configuration/implementation issue that is a root cause for many ASP.NET vulnerabilities. Mass assignment vulnerabilities in ASP.NET applications often result from improper model validation.
Related CWEs
- CWE-1173: Improper Use of Validation Framework (parent)
- CWE-20: Improper Input Validation (related)
- CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes (mass assignment)
References
- MITRE Corporation. "CWE-1174: ASP.NET Misconfiguration: Improper Model Validation." https://cwe.mitre.org/data/definitions/1174.html
- Microsoft Docs - Model Validation in ASP.NET Core MVC
- OWASP - Mass Assignment