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.IsValidshould 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
IValidatableObjectfor 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.TryUpdateModelcan 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
ValidationAttributeclasses for business-specific rules
Configure Global Validation Settings
Set up application-wide validation configuration:
- Disable automatic validation if needed: Only disable
SuppressModelStateInvalidFilterwhen using custom validation - Configure validation metadata: Use
ModelMetadataTypeattribute 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 aModelState.IsValidcheck 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.IsValidblocks 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.