.NET Input Validation
Input validation is one of the most fundamental security concepts in application development. When you build applications that accept user input—whether from web forms, command-line interfaces, or API endpoints—you need to verify that the data meets your expectations before processing it. Proper input validation helps protect your application from security vulnerabilities, unexpected errors, and data corruption.
Why Input Validation Matters
Without proper validation, your application is vulnerable to:
- Injection attacks (SQL, LDAP, OS commands, etc.)
- Cross-Site Scripting (XSS) attacks
- Buffer overflows
- Business logic errors
- Data corruption
In this guide, you'll learn various techniques to validate input in .NET applications.
Basic Input Validation Principles
When validating input, follow these key principles:
- Validate on the server side - Client-side validation is for user experience, server-side validation is for security
- Whitelist, don't blacklist - Accept only known good input rather than trying to block bad input
- Validate for type, length, format, and range
- Sanitize after validation if you need to preserve some user content
Input Validation Techniques in .NET
Let's explore different approaches to input validation in .NET applications.
1. Manual Validation
The most basic form of validation is to write custom code that checks input values:
public void ProcessUserInput(string username, int age)
{
// String validation
if (string.IsNullOrWhiteSpace(username))
{
throw new ArgumentException("Username cannot be empty", nameof(username));
}
if (username.Length > 50)
{
throw new ArgumentException("Username cannot exceed 50 characters", nameof(username));
}
// Numeric validation
if (age < 13 || age > 120)
{
throw new ArgumentException("Age must be between 13 and 120", nameof(age));
}
// Process valid input...
Console.WriteLine($"Processing user: {username}, age: {age}");
}
This approach works but can become tedious and error-prone for complex validation needs.
2. Regular Expressions
For pattern-based validation, regular expressions are powerful:
public bool IsValidEmail(string email)
{
if (string.IsNullOrWhiteSpace(email))
return false;
// Simple regex for email validation
var pattern = @"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$";
return Regex.IsMatch(email, pattern);
}
// Usage
var userEmail = "[email protected]";
if (IsValidEmail(userEmail))
{
Console.WriteLine("Email is valid");
}
else
{
Console.WriteLine("Invalid email format");
}
// Output: Email is valid
Security Note: Be careful with regular expressions on untrusted input. Poorly designed regexes can lead to performance issues or even Denial of Service attacks through "catastrophic backtracking".
3. Data Annotations
In .NET, Data Annotations provide a declarative way to specify validation rules:
using System.ComponentModel.DataAnnotations;
public class UserRegistration
{
[Required(ErrorMessage = "Username is required")]
[StringLength(50, MinimumLength = 3, ErrorMessage = "Username must be between 3 and 50 characters")]
[RegularExpression(@"^[a-zA-Z0-9_-]+$", ErrorMessage = "Username can only contain letters, numbers, underscores and hyphens")]
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(100, MinimumLength = 8, ErrorMessage = "Password must be at least 8 characters")]
[DataType(DataType.Password)]
public string Password { get; set; }
[Range(13, 120, ErrorMessage = "Age must be between 13 and 120")]
public int Age { get; set; }
}
To validate an instance:
public bool ValidateUser(UserRegistration user)
{
var context = new ValidationContext(user, serviceProvider: null, items: null);
var results = new List<ValidationResult>();
bool isValid = Validator.TryValidateObject(user, context, results, validateAllProperties: true);
if (!isValid)
{
foreach (var error in results)
{
Console.WriteLine(error.ErrorMessage);
}
}
return isValid;
}
// Usage
var newUser = new UserRegistration
{
Username = "john123",
Email = "[email protected]",
Password = "SecurePassword123",
Age = 25
};
if (ValidateUser(newUser))
{
Console.WriteLine("User input is valid");
}
// Output: User input is valid
4. Model Validation in ASP.NET Core
ASP.NET Core handles validation automatically when you use data annotations:
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
[HttpPost]
public IActionResult Register(UserRegistration model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// Process registration...
return Ok(new { message = "Registration successful" });
}
}
This automatically validates incoming requests against your data annotations.
5. FluentValidation Library
For more complex validation scenarios, consider the popular third-party FluentValidation library:
using FluentValidation;
public class UserValidator : AbstractValidator<UserRegistration>
{
public UserValidator()
{
RuleFor(x => x.Username)
.NotEmpty().WithMessage("Username is required")
.Length(3, 50).WithMessage("Username must be between 3 and 50 characters")
.Matches(@"^[a-zA-Z0-9_-]+$").WithMessage("Username can only contain letters, numbers, underscores and hyphens");
RuleFor(x => x.Email)
.NotEmpty().WithMessage("Email is required")
.EmailAddress().WithMessage("Invalid email format");
RuleFor(x => x.Password)
.NotEmpty().WithMessage("Password is required")
.MinimumLength(8).WithMessage("Password must be at least 8 characters")
.Must(HasValidPassword).WithMessage("Password must contain at least one uppercase letter, one lowercase letter, and one number");
RuleFor(x => x.Age)
.InclusiveBetween(13, 120).WithMessage("Age must be between 13 and 120");
}
private bool HasValidPassword(string password)
{
return password != null &&
password.Any(char.IsUpper) &&
password.Any(char.IsLower) &&
password.Any(char.IsDigit);
}
}
// Usage
var validator = new UserValidator();
var user = new UserRegistration { /* properties */ };
var result = validator.Validate(user);
if (!result.IsValid)
{
foreach (var error in result.Errors)
{
Console.WriteLine(error.ErrorMessage);
}
}
Validating Different Types of Input
Numeric Input Validation
// Validating integers with range check
public int ValidateQuantity(string input)
{
if (!int.TryParse(input, out int quantity))
{
throw new ArgumentException("Quantity must be a valid number");
}
if (quantity < 1 || quantity > 100)
{
throw new ArgumentException("Quantity must be between 1 and 100");
}
return quantity;
}
// Validating decimal with culture-specific formatting
public decimal ValidatePrice(string input)
{
if (!decimal.TryParse(input, NumberStyles.Currency, CultureInfo.CurrentCulture, out decimal price))
{
throw new ArgumentException("Price must be a valid currency amount");
}
if (price < 0 || price > 10000)
{
throw new ArgumentException("Price must be between 0 and 10,000");
}
return price;
}
Date Validation
public DateTime ValidateBirthDate(string input)
{
if (!DateTime.TryParse(input, out DateTime date))
{
throw new ArgumentException("Invalid date format");
}
// Check if date is in the past
if (date > DateTime.Today)
{
throw new ArgumentException("Birth date cannot be in the future");
}
// Check if person is at least 13 years old
int age = DateTime.Today.Year - date.Year;
if (date > DateTime.Today.AddYears(-age)) age--;
if (age < 13)
{
throw new ArgumentException("User must be at least 13 years old");
}
return date;
}
File Upload Validation
public async Task<bool> ValidateImageUpload(IFormFile file, int maxSizeInMb)
{
// Check if file exists
if (file == null || file.Length == 0)
{
throw new ArgumentException("No file was uploaded");
}
// Check file size
if (file.Length > maxSizeInMb * 1024 * 1024)
{
throw new ArgumentException($"File size exceeds the limit of {maxSizeInMb}MB");
}
// Check file extension
var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif" };
var fileExtension = Path.GetExtension(file.FileName).ToLowerInvariant();
if (!allowedExtensions.Contains(fileExtension))
{
throw new ArgumentException("Invalid file type. Only JPG, PNG, and GIF images are allowed");
}
// Validate content type
if (!file.ContentType.StartsWith("image/"))
{
throw new ArgumentException("Uploaded file is not a valid image");
}
// For extra security, verify the file contents match the expected image format
// This helps prevent extension spoofing
try
{
using var stream = file.OpenReadStream();
using var image = await Image.LoadAsync(stream);
// If we get here, it's a valid image format that the system can read
return true;
}
catch
{
throw new ArgumentException("The file is not a valid image");
}
}
Sanitizing Input
Validation checks if input is valid, but sometimes you need to preserve user input while making it safe:
public string SanitizeHtmlInput(string input)
{
if (string.IsNullOrEmpty(input))
{
return input;
}
// Use a library like HtmlSanitizer
var sanitizer = new HtmlSanitizer();
// Configure allowed tags and attributes
sanitizer.AllowedTags.Add("b");
sanitizer.AllowedTags.Add("i");
sanitizer.AllowedTags.Add("p");
sanitizer.AllowedTags.Add("br");
// Sanitize the input
return sanitizer.Sanitize(input);
}
Best Practices for Input Validation
- Centralize validation logic to ensure consistent rules across your application
- Don't trust client-side validation - always validate on the server
- Fail securely - reject input that doesn't meet validation criteria
- Log validation failures to detect potential attacks
- Use parameterized queries for database operations
- Use appropriate encoding when displaying user input
- Apply the principle of least privilege when processing input
Real-World Example: User Registration Form
Let's put everything together in a practical ASP.NET Core Web API example:
// UserController.cs
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
private readonly IUserService _userService;
private readonly ILogger<UserController> _logger;
public UserController(IUserService userService, ILogger<UserController> logger)
{
_userService = userService;
_logger = logger;
}
[HttpPost("register")]
public async Task<IActionResult> Register(UserRegistrationDto model)
{
try
{
// ASP.NET Core handles basic validation through attributes
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// Additional custom validation
if (await _userService.UserNameExistsAsync(model.Username))
{
ModelState.AddModelError("Username", "This username is already taken");
return BadRequest(ModelState);
}
if (await _userService.EmailExistsAsync(model.Email))
{
ModelState.AddModelError("Email", "This email is already registered");
return BadRequest(ModelState);
}
// Process registration
var result = await _userService.RegisterUserAsync(model);
return Ok(new { message = "Registration successful", userId = result.UserId });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during user registration");
return StatusCode(500, "An error occurred during registration");
}
}
}
// UserRegistrationDto.cs
public class UserRegistrationDto
{
[Required(ErrorMessage = "Username is required")]
[StringLength(50, MinimumLength = 3, ErrorMessage = "Username must be between 3 and 50 characters")]
[RegularExpression(@"^[a-zA-Z0-9_-]+$", ErrorMessage = "Username can only contain letters, numbers, underscores and hyphens")]
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(100, MinimumLength = 8, ErrorMessage = "Password must be at least 8 characters")]
[DataType(DataType.Password)]
public string Password { get; set; }
[Required(ErrorMessage = "Please confirm your password")]
[Compare("Password", ErrorMessage = "Passwords do not match")]
[DataType(DataType.Password)]
public string ConfirmPassword { get; set; }
[Range(13, 120, ErrorMessage = "Age must be between 13 and 120")]
public int Age { get; set; }
}
Summary
Input validation is a critical security measure for all applications, especially those that are publicly accessible. In this guide, we've explored:
- The principles of input validation
- Various validation techniques in .NET
- How to validate different types of input
- Sanitization approaches
- Best practices and real-world examples
By implementing proper input validation, you can protect your application from many common security vulnerabilities and ensure data integrity.
Additional Resources
- Microsoft Documentation on Data Validation
- OWASP Input Validation Cheat Sheet
- FluentValidation Library
- HtmlSanitizer
Exercises
- Create a registration form that validates all fields using Data Annotations.
- Implement FluentValidation for a contact form with custom validation rules.
- Build a file upload feature with comprehensive validation for document files.
- Create a validation utility class that handles common validation needs in your application.
- Implement server-side validation for a multi-step form process.
By practicing these exercises, you'll strengthen your understanding of input validation in .NET applications and be better equipped to create secure, robust software.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)