skills/security/input-sanitization/SKILL.md
Use when preventing XSS, adding CSP headers, or validating file uploads.
npx skillsauth add faysilalshareef/dotnet-ai-kit input-sanitizationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use the HtmlSanitizer NuGet package when you must accept rich HTML input (e.g., blog posts, comments). Never write your own regex-based sanitizer.
dotnet add package HtmlSanitizer
using Ganss.Xss;
public interface IContentSanitizer
{
string Sanitize(string untrustedHtml);
}
public sealed class ContentSanitizer : IContentSanitizer
{
private readonly HtmlSanitizer _sanitizer;
public ContentSanitizer()
{
_sanitizer = new HtmlSanitizer();
// Explicitly allow only safe tags
_sanitizer.AllowedTags.Clear();
_sanitizer.AllowedTags.Add("p");
_sanitizer.AllowedTags.Add("br");
_sanitizer.AllowedTags.Add("strong");
_sanitizer.AllowedTags.Add("em");
_sanitizer.AllowedTags.Add("ul");
_sanitizer.AllowedTags.Add("ol");
_sanitizer.AllowedTags.Add("li");
_sanitizer.AllowedTags.Add("a");
_sanitizer.AllowedTags.Add("code");
_sanitizer.AllowedTags.Add("pre");
_sanitizer.AllowedTags.Add("blockquote");
// Restrict attributes
_sanitizer.AllowedAttributes.Clear();
_sanitizer.AllowedAttributes.Add("href");
_sanitizer.AllowedAttributes.Add("class");
// Only allow safe URI schemes
_sanitizer.AllowedSchemes.Clear();
_sanitizer.AllowedSchemes.Add("https");
_sanitizer.AllowedSchemes.Add("mailto");
}
public string Sanitize(string untrustedHtml)
=> _sanitizer.Sanitize(untrustedHtml);
}
// Registration
builder.Services.AddSingleton<IContentSanitizer, ContentSanitizer>();
Razor auto-encodes @ expressions — the danger is bypassing it.
// SAFE — Razor auto-encodes this
<p>@Model.UserComment</p>
// DANGEROUS — renders raw HTML, enabling XSS
<p>@Html.Raw(Model.UserComment)</p>
// SAFE — only use Html.Raw with pre-sanitized content
<p>@Html.Raw(Model.SanitizedComment)</p>
For manual encoding in services or APIs:
using System.Text.Encodings.Web;
public sealed class NotificationService(HtmlEncoder htmlEncoder)
{
public string BuildSafeHtml(string userName, string message)
{
var safeName = htmlEncoder.Encode(userName);
var safeMessage = htmlEncoder.Encode(message);
return $"<p><strong>{safeName}</strong>: {safeMessage}</p>";
}
}
// For JavaScript contexts
var jsEncoder = JavaScriptEncoder.Default;
var safeValue = jsEncoder.Encode(userInput);
// For URL contexts
var urlEncoder = UrlEncoder.Default;
var safeParam = urlEncoder.Encode(userInput);
Content-Security-Policy is the strongest browser-side XSS mitigation. Start strict, loosen only when needed.
// Program.cs — Middleware approach
app.Use(async (context, next) =>
{
context.Response.Headers.Append(
"Content-Security-Policy",
"default-src 'self'; " +
"script-src 'self'; " +
"style-src 'self'; " +
"img-src 'self' data: https:; " +
"font-src 'self'; " +
"connect-src 'self'; " +
"frame-ancestors 'none'; " +
"base-uri 'self'; " +
"form-action 'self'");
await next();
});
For pages that require inline scripts, use nonce-based CSP:
public sealed class CspNonceMiddleware(RequestDelegate next)
{
public async Task InvokeAsync(HttpContext context)
{
var nonce = Convert.ToBase64String(
RandomNumberGenerator.GetBytes(16));
context.Items["CspNonce"] = nonce;
context.Response.Headers.Append(
"Content-Security-Policy",
$"default-src 'self'; " +
$"script-src 'self' 'nonce-{nonce}'; " +
$"style-src 'self' 'nonce-{nonce}'; " +
$"img-src 'self' data: https:; " +
$"frame-ancestors 'none'; " +
$"base-uri 'self'");
await next(context);
}
}
// In Razor views
<script nonce="@Context.Items["CspNonce"]">
// Inline script allowed by nonce
</script>
// Registration
app.UseMiddleware<CspNonceMiddleware>();
Add all recommended security headers in a single middleware:
public sealed class SecurityHeadersMiddleware(RequestDelegate next)
{
public async Task InvokeAsync(HttpContext context)
{
var headers = context.Response.Headers;
// Prevent MIME-type sniffing
headers.Append("X-Content-Type-Options", "nosniff");
// Prevent clickjacking
headers.Append("X-Frame-Options", "DENY");
// Legacy XSS filter — set to 0 to avoid
// false positives in older browsers
headers.Append("X-XSS-Protection", "0");
// Control referrer information leakage
headers.Append("Referrer-Policy",
"strict-origin-when-cross-origin");
// Enforce HTTPS
headers.Append("Strict-Transport-Security",
"max-age=31536000; includeSubDomains");
// Restrict browser features
headers.Append("Permissions-Policy",
"camera=(), microphone=(), geolocation=()");
await next(context);
}
}
// Registration — place before other middleware
app.UseMiddleware<SecurityHeadersMiddleware>();
Never trust the file extension or Content-Type header alone. Validate magic bytes, enforce size limits, and store outside the web root.
public sealed class FileUploadValidator
{
private static readonly Dictionary<string, byte[]> MagicBytes = new()
{
[".png"] = [0x89, 0x50, 0x4E, 0x47],
[".jpg"] = [0xFF, 0xD8, 0xFF],
[".gif"] = [0x47, 0x49, 0x46, 0x38],
[".pdf"] = [0x25, 0x50, 0x44, 0x46],
};
private static readonly HashSet<string> AllowedExtensions =
[".png", ".jpg", ".jpeg", ".gif", ".pdf"];
private const long MaxFileSizeBytes = 10 * 1024 * 1024; // 10 MB
public Result Validate(IFormFile file)
{
// Check size
if (file.Length == 0)
return Result.Failure("File is empty.");
if (file.Length > MaxFileSizeBytes)
return Result.Failure(
$"File exceeds {MaxFileSizeBytes / 1024 / 1024} MB limit.");
// Check extension (case-insensitive)
var extension = Path.GetExtension(file.FileName)
.ToLowerInvariant();
if (!AllowedExtensions.Contains(extension))
return Result.Failure(
$"Extension '{extension}' is not allowed.");
// Verify magic bytes match claimed extension
if (MagicBytes.TryGetValue(
extension == ".jpeg" ? ".jpg" : extension,
out var expected))
{
using var stream = file.OpenReadStream();
var header = new byte[expected.Length];
if (stream.Read(header, 0, header.Length) < header.Length
|| !header.AsSpan().StartsWith(expected))
{
return Result.Failure(
"File content does not match its extension.");
}
}
return Result.Success();
}
}
Choose the right strategy based on the data type:
// VALIDATION — reject bad input (preferred for structured data)
public sealed class CreateUserRequestValidator
: AbstractValidator<CreateUserRequest>
{
public CreateUserRequestValidator()
{
RuleFor(x => x.Email).NotEmpty().EmailAddress().MaximumLength(256);
RuleFor(x => x.DisplayName).NotEmpty().MaximumLength(100)
.Matches(@"^[\w\s\-'.]+$")
.WithMessage("Display name contains invalid characters.");
}
}
// SANITIZATION — clean but accept (for rich content)
public string ProcessComment(string rawHtml)
{
if (rawHtml.Length > 50_000)
throw new ValidationException("Comment too long.");
return _contentSanitizer.Sanitize(rawHtml);
}
| Data Type | Strategy | Rationale |
|-----------|----------|-----------|
| Email, phone, ID | Validate and reject | Structured format — any deviation is invalid |
| Display name | Validate with pattern | No HTML needed — reject outside [\w\s\-'.] |
| Plain-text comment | Validate length, encode on output | No markup needed — encoding prevents XSS |
| Rich-text / HTML | Validate length, sanitize HTML | User needs formatting — allow safe subset |
| File upload | Validate extension, size, magic bytes | Binary content — cannot sanitize, must verify |
| Scenario | Primary Defense | Supporting Layers |
|----------|----------------|-------------------|
| Razor view rendering user data | Auto-encoding (@) | CSP, security headers |
| API returning HTML content | HtmlSanitizer | CSP on consuming SPA |
| SPA with user-generated content | CSP script-src 'self' | Output encoding in API |
| File uploads | Extension + magic byte validation | Size limits, antivirus scan |
| Form submissions | FluentValidation / DataAnnotations | Anti-forgery tokens |
| Rich-text editor | HtmlSanitizer with tag allowlist | CSP, output encoding |
| Inline scripts required | Nonce-based CSP | Security headers, SRI |
| Third-party scripts | script-src with hash or domain | Subresource Integrity (SRI) |
| Anti-Pattern | Problem | Correct Approach |
|--------------|---------|------------------|
| @Html.Raw(userInput) | Renders unescaped HTML — direct XSS | Sanitize first or use @ auto-encoding |
| Regex-based HTML sanitizer | Impossible to cover all edge cases | Use HtmlSanitizer NuGet package |
| Content-Security-Policy: unsafe-inline | Defeats the purpose of CSP entirely | Use nonce-based or hash-based CSP |
| Checking only file extension | Attacker renames .exe to .jpg | Validate magic bytes and extension together |
| Trusting Content-Type header | Client controls this header | Check magic bytes server-side |
| Sanitizing on input, rendering raw | Data may be modified after sanitization | Encode/sanitize at the point of output |
| Blocklist approach (strip <script>) | Countless bypass vectors exist | Use allowlist of safe tags and attributes |
| No size limit on uploads | Denial of service via large files | Enforce MaxFileSizeBytes and RequestSizeLimit |
| Missing X-Content-Type-Options | Browser may MIME-sniff responses as HTML | Always send nosniff header |
| X-XSS-Protection: 1; mode=block | Can introduce side-channel attacks in older browsers | Set to 0 and rely on CSP instead |
Html.Raw usage across Razor views — each occurrence needs reviewProgram.cs for Content-Security-Policy header or CSP middlewareHtmlSanitizer NuGet in .csproj filesX-Content-Type-Options to verify security headers are setHtmlEncoder, JavaScriptEncoder, or UrlEncoder usageHtml.Raw calls — replace with sanitized output or auto-encodingReport-Only mode, then enforceHtmlSanitizer if the app accepts rich HTML contentdata-ai
Use when about to claim work is complete, fixed, passing, or ready — before committing, creating PRs, or moving to the next task. Requires running verification commands and confirming output before making any success claims.
development
Use when encountering any bug, test failure, build error, or unexpected behavior — before proposing fixes or making changes.
development
Use when checkpointing, wrapping up, or handing off an AI-assisted development session.
development
Use when following the Specification-Driven Development lifecycle from plan through ship.