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

ImpactDetails
IntegrityScope: 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.


  • 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

  1. MITRE Corporation. "CWE-1174: ASP.NET Misconfiguration: Improper Model Validation." https://cwe.mitre.org/data/definitions/1174.html
  2. Microsoft Docs - Model Validation in ASP.NET Core MVC
  3. OWASP - Mass Assignment