Cross-site scripting (XSS) vulnerabilities occur when malicious scripts are injected into web pages viewed by other users. To mitigate this risk, applications must sanitize user inputs to ensure that only trusted HTML content is rendered. In many scenarios, completely blocking all HTML tags is impractical; instead, developers need granular control to permit specific tags or attributes while stripping out potential threats.
The following implementation provides a custom validation attribute, XssProtectionAttribute, which sits within the App.Security.Mvc namespace. This attribute leverages the ValidationAttribute base class to inspect incoming model properties. It encodes input strings and compares them against the original value. If a difference is detected, it checks against a whitelist of permitted HTML strings. Additionally, it supports a blacklist mechanism to explicitly forbid specific characters or sequences.
Usage Example
public class UserProfileViewModel
{
// Blocks all HTML tags by default
[XssProtection]
public string Biography { get; set; }
// Allows specific formatting tags
[XssProtection(permittedHtmlTags: "<br/>,<p>,<strong>")]
public string Comments { get; set; }
// Permits line breaks but explicitly blocks slashes and hashes
[XssProtection(permittedHtmlTags: "<br/>", forbiddenSequences: "/, #")]
public string Nickname { get; set; }
// Uses a custom error message
[XssProtection(errorText: "Invalid characters detected in the input field.")]
public string Signature { get; set; }
// Localized error message via resources
[XssProtection(errorResourceName: "ValidationError", errorResourceType: typeof(LocalizationResources))]
public string Location { get; set; }
}Source Implementation
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using System.Resources;
namespace App.Security.Mvc
{
/// <summary>
/// Validates input to prevent XSS attacks by encoding HTML and checking against white/blacklists.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class XssProtectionAttribute : ValidationAttribute
{
private const string DefaultErrorFormat = "The field {0} contains potentially unsafe content.";
private readonly string _customErrorMessage;
private readonly string _resourceName;
private readonly Type _resourceType;
private readonly string _whitelist;
private readonly string _blacklist;
private Dictionary<string, string> _safeTagMappings;
public XssProtectionAttribute(
string errorText = null,
string errorResourceName = null,
Type errorResourceType = null,
string permittedHtmlTags = null,
string forbiddenSequences = null)
{
_customErrorMessage = errorText;
_resourceName = errorResourceName;
_resourceType = errorResourceType;
_whitelist = permittedHtmlTags;
_blacklist = forbiddenSequences;
_safeTagMappings = new Dictionary<string, string>();
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value == null || string.IsNullOrWhiteSpace(value.ToString()))
{
return ValidationResult.Success;
}
var rawInput = value.ToString();
var sanitizedInput = HtmlSanitizer.Encode(rawInput);
if (rawInput != sanitizedInput)
{
InitializeSafeTagMappings();
foreach (var mapping in _safeTagMappings)
{
// Restore whitelisted tags to see if input is now safe
sanitizedInput = sanitizedInput.Replace(mapping.Value, mapping.Key);
}
// If restoring whitelisted tags doesn't match the original, other unsafe content exists
if (rawInput != sanitizedInput)
{
return new ValidationResult(GetErrorMessage(validationContext));
}
}
// Check for explicitly forbidden sequences
if (!string.IsNullOrWhiteSpace(_blacklist))
{
var forbiddenItems = _blacklist.Split(',').Select(s => s.Trim()).ToList();
if (forbiddenItems.Any(item => rawInput.Contains(item)))
{
return new ValidationResult(GetErrorMessage(validationContext));
}
}
return ValidationResult.Success;
}
private void InitializeSafeTagMappings()
{
if (string.IsNullOrWhiteSpace(_whitelist)) return;
foreach (var tag in _whitelist.Split(',').Select(t => t.Trim()))
{
if (!_safeTagMappings.ContainsKey(tag))
{
_safeTagMappings.Add(tag, HtmlSanitizer.Encode(tag));
}
}
}
private string GetErrorMessage(ValidationContext context)
{
if (!string.IsNullOrEmpty(_resourceName) && _resourceType != null)
{
var manager = new ResourceManager(_resourceType);
return manager.GetString(_resourceName, CultureInfo.CurrentUICulture) ?? string.Format(DefaultErrorFormat, context.DisplayName);
}
if (!string.IsNullOrEmpty(_customErrorMessage))
{
return _customErrorMessage;
}
return string.Format(DefaultErrorFormat, context.DisplayName);
}
}
// Internal helper for encoding (assumes existence of a utility or uses System.Web)
internal static class HtmlSanitizer
{
public static string Encode(string input)
{
// Placeholder for actual encoding logic, e.g., System.Web.HttpUtility.HtmlEncode
return System.Net.WebUtility.HtmlEncode(input);
}
}
}