Skip to content

CWE-1174: ASP.NET Misconfiguration: Improper Model Validation

Overview

ASP.NET Misconfiguration: Improper Model Validation occurs when ASP.NET applications fail to properly validate model data, allowing attackers to bypass security controls or inject malicious data. This vulnerability arises when the application doesn't properly configure or use ASP.NET's built-in model validation features.

While CWE-1174 specifically covers ASP.NET model validation misconfiguration, related issues are often classified under CWE-20 (Improper Input Validation) and CWE-915 (Mass Assignment).

Defense-in-depth: CWE-1174 (validation) and CWE-915 (mass assignment) work together - CWE-915 controls which properties can be set (e.g., using ViewModels to exclude IsAdmin), while CWE-1174 validates the values of allowed properties (e.g., using [EmailAddress] to validate email format). Both protections are essential.

OWASP Classification

A02:2025 - Security Misconfiguration: ASP.NET misconfiguration allowing improper model validation

Risk

This vulnerability can lead to:

  • Mass assignment attacks where attackers modify unintended object properties
  • Data integrity issues from accepting invalid or malicious input
  • Business logic bypasses through manipulated model properties
  • Security control bypasses when validation is incomplete or misconfigured

Remediation Steps

Core principle: Enforce server-side model validation consistently; reject invalid models before use and do not rely on client-side validation.

Enable Model Validation

Ensure ASP.NET model validation is properly enabled and configured:

  • Check ModelState.IsValid: ModelState.IsValid should be used for MVC applications before processing model data
  • Return validation errors: Return appropriate error responses when validation fails
  • Use validation attributes: Apply [Required], [StringLength], [Range], etc. to model properties
  • Custom validation: Implement IValidatableObject for complex validation logic

Prevent Mass Assignment Attacks

Protect against over-posting vulnerabilities:

  • Use BindAttribute: Apply [Bind] with an allowlist of properties (syntax varies by MVC vs Core).
  • Use ViewModels/DTOs: Create separate models for input that only contain properties users should modify
  • Avoid binding to entities directly: Don't bind user input directly to database entity models
  • Use TryUpdateModel selectively: When using TryUpdateModel, specify the exact properties to update. TryUpdateModel can be used safely only when explicit property allowlists are supplied; otherwise it is vulnerable to over-posting.

Implement Strong Type Validation

Use robust data type validation:

  • Data annotations: Use [RegularExpression], [EmailAddress], [Url] attributes
  • Range validation: Apply [Range] for numeric values with min/max constraints
  • String length limits: Use [StringLength] and [MaxLength] to prevent resource exhaustion
  • Custom validators: Create custom ValidationAttribute classes for business-specific rules

Configure Global Validation Settings

Set up application-wide validation configuration:

  • Disable automatic validation if needed: Only disable SuppressModelStateInvalidFilter when using custom validation
  • Configure validation metadata: Use ModelMetadataType attribute to separate validation from models
  • Client-side validation: Enable unobtrusive client validation, but always validate server-side
  • ValidateAntiForgeryToken: Combine with anti-CSRF tokens to prevent automated attacks

Sanitize and Encode Output

Protect against injection even after validation:

  • HTML encode output: Use @Html.Encode() or Razor's automatic encoding
  • Parameterized queries: Use parameterized SQL/LINQ to prevent SQL injection
  • Validate and sanitize: Validate input; encode output according to context. Sanitize only when intentionally accepting rich content (e.g., HTML) using a robust sanitizer
  • Content Security Policy: Implement CSP headers to mitigate XSS risks

Test Validation Logic

Verify validation works correctly:

  • Unit test validators: Test each validation attribute and custom validator
  • Test boundary conditions: Verify validation at min/max limits, empty strings, null values
  • Test bypass attempts: Try to submit invalid data, extra properties, manipulated values
  • Integration tests: Test full request/response cycle with invalid models
  • Security testing: Use tools to test for mass assignment and validation bypass vulnerabilities

Common Vulnerable Patterns

Missing ModelState Validation

NOTE: By default, controllers using [ApiController] automatically return HTTP 400 on model validation failure, unless that behavior is explicitly suppressed or overridden.

// VULNERABLE - No ModelState validation
[HttpPost]
public IActionResult UpdateProfile(UserProfileModel model)
{
    // No ModelState.IsValid check!
    _userService.UpdateProfile(model);
    return Ok();
}

// Attack: POST with invalid data bypasses all validation
// { "Email": "not-an-email", "Age": -5, "Bio": "<script>alert('xss')</script>" }

Why this is vulnerable:

  • Validation attributes only populate ModelState.Errors; without a ModelState.IsValid check the action still runs.
  • Malformed input can violate business rules or cause downstream errors because code assumes validated data.
  • Injection payloads or invalid values can reach services, storage, or other processing paths.

Mass Assignment Attack

// VULNERABLE - Direct entity binding allows over-posting
[HttpPost]
public IActionResult UpdateUser(User user)  // User is a database entity
{
    if (ModelState.IsValid)
    {
        _db.Users.Update(user);
        _db.SaveChanges();
        return Ok();
    }
    return BadRequest();
}

// Attack: POST with extra properties
// { "Id": 123, "Name": "Alice", "Email": "alice@example.com", "IsAdmin": true, "Balance": 999999 }
// Attacker sets IsAdmin=true or manipulates Balance

Why this is vulnerable:

  • The model binder maps any request field to matching entity properties by default.
  • Attackers can submit hidden or extra fields (e.g., IsAdmin, Balance) that are not in the UI.
  • Over-posting can lead to privilege escalation, data corruption, or business logic bypasses.

Weak Validation Attributes

// VULNERABLE - Insufficient validation constraints
public class ProductModel
{
    [Required]
    public string Name { get; set; }  // No max length!

    public decimal Price { get; set; }  // No range constraint!

    [EmailAddress]
    public string Email { get; set; }  // No required attribute!
}

// Attack examples:
// - Name: 1MB string (DoS / memory pressure / downstream storage issues)
// - Price: -999999 (negative price)
// - Email: null or "" (bypass validation)

Why this is vulnerable:

  • Missing length limits allows extremely long input that can exhaust memory or storage.
  • Missing range constraints permits negative or unrealistic values that break business rules.
  • Optional fields without [Required] allow empty values that the system may assume are present.

Client-Side Only Validation

// VULNERABLE - Relying on client-side validation
[HttpPost]
public IActionResult CreateAccount(AccountModel model)
{
    // Assumes client-side validation caught errors
    _accountService.Create(model);
    return Ok();
}

// No server-side validation!
// Attackers bypass client-side checks using curl, Postman, or browser dev tools

Why this is vulnerable:

  • Client-side checks can be bypassed with direct HTTP requests (curl, Postman, Burp).
  • JavaScript or DOM constraints can be altered or disabled in the browser.
  • Without server-side validation, malformed input reaches trusted processing paths.

Missing Bind Attribute Protection

// VULNERABLE - No property restrictions
[HttpPost]
public async Task<IActionResult> Register(UserAccount account)
{
    if (ModelState.IsValid)
    {
        await _db.UserAccounts.AddAsync(account);
        await _db.SaveChangesAsync();
        return Ok();
    }
    return BadRequest();
}

// UserAccount entity has: Username, Password, Email, IsAdmin, AccountBalance, CreatedDate
// Attack: POST { "Username": "hacker", "Password": "pass", "Email": "x@y.com", "IsAdmin": true }

Why this is vulnerable:

  • The binder accepts any request property that matches the entity.
  • Sensitive fields (e.g., IsAdmin, AccountBalance, CreatedDate) become user-controllable.
  • Attackers can escalate privileges or corrupt data during registration.

Secure Patterns

Always Check ModelState.IsValid

// SECURE - Proper ModelState validation
[HttpPost]
public IActionResult UpdateProfile(UserProfileModel model)
{
    if (!ModelState.IsValid)
    {
        // Return validation errors to client
        return BadRequest(ModelState);
    }

    _userService.UpdateProfile(model);
    return Ok();
}

// Model with validation attributes
public class UserProfileModel
{
    [Required]
    [StringLength(100, MinimumLength = 2)]
    public string Name { get; set; }

    [Required]
    [EmailAddress]
    public string Email { get; set; }

    [Range(13, 120)]
    public int Age { get; set; }

    [StringLength(500)]
    public string Bio { get; set; }
}

Why this works:

  • ModelState.IsValid blocks processing when any validation rule fails.
  • Data annotations are evaluated during model binding and populate ModelState.Errors.
  • BadRequest(ModelState) returns structured 400 responses with error details.
  • Invalid data is rejected before it reaches business logic or storage.

Use ViewModels to Prevent Mass Assignment

// SECURE - ViewModel with only allowed properties
public class UpdateUserViewModel
{
    [Required]
    [StringLength(100)]
    public string Name { get; set; }

    [Required]
    [EmailAddress]
    public string Email { get; set; }
}

[HttpPost]
public IActionResult UpdateUser(int id, UpdateUserViewModel viewModel)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    // Fetch entity from database
    var user = _db.Users.Find(id);
    if (user == null)
    {
        return NotFound();
    }

    // Manually map only allowed properties
    user.Name = viewModel.Name;
    user.Email = viewModel.Email;
    // user.IsAdmin is NOT updated - not in ViewModel

    _db.SaveChanges();
    return Ok();
}

Why this works:

  • The ViewModel exposes only fields the user is allowed to change.
  • Extra request fields are ignored because they do not exist on the ViewModel.
  • Manual mapping keeps updates explicit and limited to approved properties.
  • Validation attributes are scoped to the specific operation.

Comprehensive Validation Attributes

// SECURE - Strong validation constraints
public class ProductModel
{
    [Required(ErrorMessage = "Product name is required")]
    [StringLength(200, MinimumLength = 3, ErrorMessage = "Name must be 3-200 characters")]
    [RegularExpression(@"^[a-zA-Z0-9\s\-]+$", ErrorMessage = "Name contains invalid characters")]
    public string Name { get; set; }

    [Required]
    [Range(0.01, 999999.99, ErrorMessage = "Price must be between $0.01 and $999,999.99")]
    [DataType(DataType.Currency)]
    public decimal Price { get; set; }

    [Required(ErrorMessage = "Email is required")]
    [EmailAddress(ErrorMessage = "Invalid email format")]
    [StringLength(100)]
    public string Email { get; set; }

    [StringLength(1000, ErrorMessage = "Description cannot exceed 1000 characters")]
    public string Description { get; set; }

    [Url(ErrorMessage = "Invalid URL format")]
    public string Website { get; set; }
}

Why this works:

  • [Required] rejects missing or empty fields.
  • [StringLength] limits input size and enforces minimum length.
  • [Range] enforces numeric bounds tied to business rules.
  • [RegularExpression] restricts accepted characters to an allowlist.
  • [EmailAddress] and [Url] validate format; they are not security controls against injection.

Custom error messages improve user experience while maintaining security. This defense-in-depth approach means attackers must bypass multiple validators, and validation failures provide clear feedback.

NOTE: [DataType] provides UI and formatting metadata and should not be relied upon for security validation.

Use Bind Attribute for Additional Protection

// SECURE - Explicit property binding control
// This Bind approach is suitable for ASP.NET Core and newer
// Use Bind(Include="") for ASP.NET MVC
[HttpPost]
public async Task<IActionResult> Register([Bind("Username,Password,Email")] UserAccount account)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    // Only Username, Password, Email are bound from request
    // IsAdmin, AccountBalance, CreatedDate are NOT bound (remain default values)

    account.IsAdmin = false;  // Explicitly set secure defaults
    account.AccountBalance = 0;
    account.CreatedDate = DateTime.UtcNow;

    await _db.UserAccounts.AddAsync(account);
    await _db.SaveChangesAsync();

    return Ok();
}

Why this works:

  • [Bind] limits binding to an explicit allowlist of properties.
  • Sensitive fields are not populated from the request and stay at safe defaults.
  • Explicitly setting defaults provides defense-in-depth.
  • ViewModels remain the preferred, clearer option for most scenarios.

Custom Validation for Complex Rules

// SECURE - Custom validation with IValidatableObject
public class OrderModel : IValidatableObject
{
    [Required]
    [Range(1, int.MaxValue)]
    public int Quantity { get; set; }

    [Required]
    [Range(0.01, 999999.99)]
    public decimal UnitPrice { get; set; }

    [Range(0, 100)]
    public decimal DiscountPercent { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        // Business rule: Total discount cannot exceed total price
        decimal totalPrice = Quantity * UnitPrice;
        decimal discountAmount = totalPrice * (DiscountPercent / 100);

        if (discountAmount > totalPrice)
        {
            yield return new ValidationResult(
                "Discount cannot exceed total price",
                new[] { nameof(DiscountPercent) }
            );
        }

        // Business rule: Large orders require manual approval (not allowed through API)
        if (Quantity > 1000)
        {
            yield return new ValidationResult(
                "Orders over 1000 units require manual approval",
                new[] { nameof(Quantity) }
            );
        }

        // Business rule: Discount only for quantities > 10
        if (DiscountPercent > 0 && Quantity < 10)
        {
            yield return new ValidationResult(
                "Discounts only available for orders of 10 or more",
                new[] { nameof(DiscountPercent), nameof(Quantity) }
            );
        }
    }
}

[HttpPost]
public IActionResult PlaceOrder(OrderModel model)
{
    if (!ModelState.IsValid)  // Includes IValidatableObject.Validate results
    {
        return BadRequest(ModelState);
    }

    _orderService.CreateOrder(model);
    return Ok();
}

Why this works:

  • IValidatableObject.Validate() enforces cross-field and calculated business rules.
  • Validation runs during model binding and adds errors to ModelState.
  • Errors are tied to specific fields for precise client feedback.
  • Invalid states are blocked before any database or business logic executes.

Additional Resources