CWE-80: Cross-Site Scripting (XSS) - C# / ASP.NET
Overview
XSS occurs when untrusted data is included in web output without proper encoding, allowing attackers to inject malicious scripts. In C#/ASP.NET applications, use the built-in encoding features and avoid raw HTML output.
Primary Defence: Use Razor's automatic HTML encoding (avoid @Html.Raw()), explicitly encode with HttpUtility.HtmlEncode() or System.Net.WebUtility.HtmlEncode() for dynamic content, implement Content Security Policy (CSP) headers, and validate/sanitize input using allowlists and regex patterns to prevent XSS attacks through proper output encoding and input validation.
Common Vulnerable Patterns
Raw String Concatenation in Razor
// VULNERABLE - No encoding
@{
var userName = ViewBag.UserName;
}
<div>Welcome, @Html.Raw(userName)</div>
// VULNERABLE - Direct concatenation
public IActionResult Index()
{
var html = "<h1>Welcome " + Request.Query["name"] + "</h1>";
return Content(html, "text/html");
}
Using HttpResponse.Write Without Encoding
// VULNERABLE
protected void Page_Load(object sender, EventArgs e)
{
string userInput = Request.QueryString["comment"];
Response.Write("<div>" + userInput + "</div>");
}
Literal Controls with User Data
JavaScript Context Without Encoding
// VULNERABLE - JavaScript injection
<script>
var message = '@ViewBag.Message';
alert(message);
</script>
Secure Patterns
Razor Automatic Encoding
// SECURE - Razor automatically HTML-encodes @ expressions
@{
var userName = ViewBag.UserName; // Could be "<script>alert('xss')</script>"
}
<div>Welcome, @userName</div>
<!-- Output: Welcome, <script>alert('xss')</script> -->
// SECURE - Razor with model binding
@model UserViewModel
<h1>Hello, @Model.UserName</h1>
<p>Email: @Model.Email</p>
Why this works: Razor's @ syntax automatically HTML-encodes all output, converting dangerous characters like <, >, &, and quotes into HTML entities (<, >, &, etc.). This prevents browsers from interpreting user input as executable HTML or JavaScript. The encoding is context-aware and happens at render time, ensuring that even complex expressions and model properties are protected. This is the default behavior in ASP.NET Core and cannot be accidentally bypassed unless you explicitly use @Html.Raw().
HttpUtility.HtmlEncode for Classic ASP.NET
// SECURE - Web Forms with explicit encoding
using System.Web;
protected void Page_Load(object sender, EventArgs e)
{
string userInput = Request.QueryString["comment"];
string encoded = HttpUtility.HtmlEncode(userInput);
Response.Write("<div>" + encoded + "</div>");
}
// SECURE - Literal control with encoding
Literal1.Text = "<p>" + HttpUtility.HtmlEncode(Request["userInput"]) + "</p>";
Why this works: HttpUtility.HtmlEncode() is the standard .NET Framework method for HTML encoding. It converts special HTML characters to their entity equivalents, preventing XSS by ensuring user input is displayed as text rather than interpreted as markup. This method is essential for classic ASP.NET WebForms applications where Razor's automatic encoding is not available. The encoding is comprehensive, handling all characters that could break out of HTML contexts including ampersands, brackets, and quotes.
Context-Specific Encoding
HTML Context
// SECURE - HTML body content
using System.Net;
using System.Text.Encodings.Web;
public string GetSafeHtml(string userInput)
{
return HtmlEncoder.Default.Encode(userInput);
}
// Usage in controller
public IActionResult Display(string message)
{
ViewBag.SafeMessage = HtmlEncoder.Default.Encode(message);
return View();
}
Why this works: HtmlEncoder.Default.Encode() is the modern .NET Core/5+ approach to HTML encoding. It provides the same security benefits as HttpUtility.HtmlEncode() but with better performance and standardization across .NET platforms. The encoder handles all HTML special characters and works consistently whether you're building strings in code or preparing data for Razor views. Using the Default encoder ensures you get Microsoft's recommended encoding rules without needing custom configuration.
JavaScript Context
// SECURE - JavaScript string context
using System.Text.Encodings.Web;
public IActionResult GetScript(string userName)
{
var jsEncodedName = JavaScriptEncoder.Default.Encode(userName);
var script = $"<script>var user = '{jsEncodedName}';</script>";
return Content(script, "text/html");
}
// SECURE - In Razor view
@using System.Text.Encodings.Web
<script>
var message = '@JavaScriptEncoder.Default.Encode(ViewBag.Message)';
console.log(message);
</script>
Why this works: JavaScript context requires different encoding than HTML. JavaScriptEncoder.Default.Encode() uses JavaScript-native escape sequences that are specifically designed for JavaScript string literals - single quotes become \u0027, double quotes become \u0022, backslashes become \\, etc. This ensures the JavaScript interpreter correctly treats the data as a string literal, not executable code. HTML encoding converts these same characters to HTML entities (', "), which is inappropriate for JavaScript contexts where you need JavaScript escape sequences, not HTML entities. For example, input like '; alert(1)// is JavaScript-encoded to \u0027; alert(1)//, which JavaScript interprets as a single string containing those literal characters, preventing the quote from breaking out of the string context and executing the alert.
URL Context
// SECURE - URL parameter encoding
using System.Text.Encodings.Web;
public string BuildUrl(string userQuery)
{
var encoded = UrlEncoder.Default.Encode(userQuery);
return $"/search?q={encoded}";
}
// SECURE - In Razor
<a href="/search?q=@UrlEncoder.Default.Encode(Model.SearchTerm)">Search</a>
Why this works: UrlEncoder.Default.Encode() percent-encodes special characters for safe inclusion in URLs. This prevents attacks where user input could inject additional query parameters or change the URL structure. For example, &admin=true becomes %26admin%3Dtrue, preserving it as data rather than URL syntax. This is critical for redirect URLs and query parameters where an attacker might try to manipulate the URL structure. The encoding follows RFC 3986 standards for URL encoding.
AntiXSS Library (Legacy)
// SECURE - For older .NET Framework projects
using Microsoft.Security.Application;
string safe = Encoder.HtmlEncode(userInput);
string safeCss = Encoder.CssEncode(cssValue);
string safeUrl = Encoder.UrlEncode(urlParam);
string safeJs = Encoder.JavaScriptEncode(jsValue);
// NuGet: Install-Package AntiXSS
Why this works:
- Context-specific encoding: Each method (
HtmlEncode,CssEncode,UrlEncode,JavaScriptEncode) applies appropriate escaping for its context, converting dangerous chars to safe representations - Comprehensive protection:
JavaScriptEncodeescapes quotes/backslashes/script-closing;CssEncodehandlesexpression()/url(); superior to basicHttpUtility.HtmlEncode() - Allowlist-based: Encodes all characters except those explicitly safe in each context, preventing bypass attempts
- Legacy relevance: Deprecated for .NET Core/5+ (use
HtmlEncoder.Default.Encode()), but essential for .NET Framework 4.x where modern APIs unavailable - Additional features: Includes
GetSafeHtml()for HTML sanitization; context-aware approach far more secure than manual encoding or generic string replacement
Alternative Encoding Methods
For legacy .NET Framework applications or specific scenarios:
HttpUtility (Classic ASP.NET):
using System.Web;
string safe = HttpUtility.HtmlEncode(userInput);
string safeUrl = HttpUtility.UrlEncode(userInput);
string safeJs = HttpUtility.JavaScriptStringEncode(userInput);
Why this works: These utility classes provide encoding methods for applications that cannot use the modern System.Text.Encodings.Web APIs. HttpUtility.HtmlEncode() converts HTML special characters to their entity equivalents, while UrlEncode() percent-encodes characters for safe URL usage, and JavaScriptStringEncode() escapes quotes and backslashes for JavaScript string contexts. While not as comprehensive as the newer APIs, these methods provide essential XSS protection for .NET Framework 4.x applications.
WebUtility (.NET Framework without System.Web):
using System.Net;
string safe = WebUtility.HtmlEncode(userInput);
string safeUrl = WebUtility.UrlEncode(userInput);
ASP.NET MVC Helpers:
using System.Web.Mvc;
string safe = HtmlHelper.Encode(userInput);
string safeAttr = HtmlHelper.AttributeEncode(userInput);
Framework-Specific Guidance
ASP.NET Core MVC (Razor)
// SECURE - Automatic encoding by default
@model CommentViewModel
<div class="comment">
<h3>@Model.Author</h3>
<p>@Model.Text</p>
<small>Posted: @Model.Timestamp</small>
</div>
// When you MUST output raw HTML (e.g., rich text editor):
// 1. Use a sanitization library
@using Ganss.Xss
@{
var sanitizer = new HtmlSanitizer();
var safeHtml = sanitizer.Sanitize(Model.RichContent);
}
@Html.Raw(safeHtml)
NuGet Packages:
ASP.NET Web API
// SECURE - JSON responses are automatically encoded
public IActionResult GetUser(int id)
{
var user = _userService.GetUser(id);
return Json(new
{
name = user.Name, // Automatically JSON-encoded
bio = user.Bio
});
}
// SECURE - Model validation and encoding
[HttpPost]
public IActionResult CreateComment([FromBody] CommentDto dto)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
var comment = new Comment
{
Text = dto.Text, // Will be HTML-encoded when rendered
Author = dto.Author
};
_db.Comments.Add(comment);
_db.SaveChanges();
return Ok(comment);
}
Blazor Server / WebAssembly
// SECURE - Razor components auto-encode
@page "/profile"
@inject UserService UserService
<h1>Profile: @currentUser.Name</h1>
<p>@currentUser.Bio</p>
@code {
private User currentUser;
protected override async Task OnInitializedAsync()
{
currentUser = await UserService.GetCurrentUser();
// All @-expressions are automatically HTML-encoded
}
}
// For dynamic markup, use MarkupString with sanitization
@using Microsoft.AspNetCore.Components
@using Ganss.Xss
@code {
private MarkupString GetSafeMarkup(string html)
{
var sanitizer = new HtmlSanitizer();
return (MarkupString)sanitizer.Sanitize(html);
}
}
<div>@GetSafeMarkup(Model.RichText)</div>
Input Validation (Defense in Depth)
using System.ComponentModel.DataAnnotations;
public class CommentDto
{
[Required]
[StringLength(1000, MinimumLength = 1)]
[RegularExpression(@"^[^<>]*$", ErrorMessage = "HTML tags not allowed")]
public string Text { get; set; }
[Required]
[StringLength(100)]
[RegularExpression(@"^[a-zA-Z0-9\s]*$", ErrorMessage = "Only alphanumeric")]
public string Author { get; set; }
}
// Controller with validation
[HttpPost]
public IActionResult PostComment([FromBody] CommentDto dto)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
// Even with validation, still encode output
ViewBag.Comment = dto.Text;
return View();
}
Content Security Policy (CSP)
// Add CSP headers in middleware
public class SecurityHeadersMiddleware
{
private readonly RequestDelegate _next;
public SecurityHeadersMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
context.Response.Headers.Add("Content-Security-Policy",
"default-src 'self'; " +
"script-src 'self' https://trusted-cdn.com; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"frame-ancestors 'none';"
);
context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
context.Response.Headers.Add("X-Frame-Options", "DENY");
context.Response.Headers.Add("X-XSS-Protection", "1; mode=block");
await _next(context);
}
}
// Startup.cs / Program.cs
public void Configure(IApplicationBuilder app)
{
app.UseMiddleware<SecurityHeadersMiddleware>();
// ... other middleware
}
Rich HTML Sanitization
When you need to allow safe HTML (e.g., from a WYSIWYG editor):
using Ganss.Xss;
public class HtmlSanitizerService
{
private readonly HtmlSanitizer _sanitizer;
public HtmlSanitizerService()
{
_sanitizer = new HtmlSanitizer();
// 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");
// Allow only safe attributes
_sanitizer.AllowedAttributes.Clear();
_sanitizer.AllowedAttributes.Add("class");
// Remove all event handlers
_sanitizer.AllowedAttributes.Remove("onclick");
_sanitizer.AllowedAttributes.Remove("onerror");
}
public string Sanitize(string html)
{
return _sanitizer.Sanitize(html);
}
}
// Usage
public IActionResult SaveArticle([FromBody] ArticleDto dto)
{
var sanitized = _htmlSanitizer.Sanitize(dto.Content);
var article = new Article
{
Title = dto.Title, // Will be encoded in view
Content = sanitized // Pre-sanitized HTML
};
_db.Articles.Add(article);
_db.SaveChanges();
return Ok();
}
// View with @Html.Raw (safe because sanitized)
@model Article
<h1>@Model.Title</h1>
<div class="content">
@Html.Raw(Model.Content)
</div>
Why this works: When you need to allow rich HTML content (like from a WYSIWYG editor), simple HTML encoding would destroy the formatting. HtmlSanitizer provides a allowlist-based approach: it parses the HTML, removes any dangerous tags (like <script>) and attributes (like onclick), and reconstructs clean HTML containing only approved elements. This allows safe formatting tags like <strong> and <p> while blocking XSS vectors. The sanitization happens before storage, so even if @Html.Raw() is used for display, the content has already been made safe. The allowlist approach is more secure than denylisting because it blocks unknown attack vectors by default.
Verification and Detection
Security testing requires multiple approaches - unit tests alone are insufficient.
Static Application Security Testing (SAST)
Use automated tools to analyze code for missing or incorrect encoding:
Commercial Tools:
- Checkmarx - Detects XSS vulnerabilities in C#/ASP.NET code
- Fortify Static Code Analyzer - Tracks data flow from user input to output
- Veracode - Identifies unencoded output in web applications
- SonarQube Enterprise - Security-focused code analysis
Open Source Tools:
- SonarQube Community - Basic XSS detection rules for C#
-
Security Code Scan - Roslyn-based analyzer for .NET
- Puma Scan - Free Visual Studio extension
Dynamic Application Security Testing (DAST)
Test running applications for XSS vulnerabilities:
- OWASP ZAP - Free, automated vulnerability scanner
- Burp Suite Professional - Comprehensive web app security testing
- Acunetix - Automated XSS detection
- Netsparker - Automated vulnerability scanner
Code Review Checklist
Manually verify:
- All
@expressions in Razor use auto-escaping (not@Html.Raw) - All
Response.Write()usesHtmlEncoder.Default.Encode() - No direct string concatenation with user input for HTML
-
@Html.Raw()only used with sanitized content - Context-appropriate encoding (HTML vs JS vs URL)
- All user input sources identified (query strings, form data, headers, database)
Framework-Specific Tools
ASP.NET Core:
# Run security analyzers
dotnet build /p:EnableNETAnalyzers=true
dotnet build /p:AnalysisLevel=latest
# Add security analyzer package
dotnet add package Microsoft.CodeAnalysis.NetAnalyzers
Security Headers Testing:
Limited Role of Unit Tests
Unit tests can verify encoding functions work correctly but cannot ensure they're used everywhere:
using Xunit;
using System.Text.Encodings.Web;
// Unit tests verify encoding functions work - NOT comprehensive security
public class EncodingFunctionTests
{
[Fact]
public void HtmlEncoder_EncodesScriptTags()
{
string malicious = "<script>alert('xss')</script>";
string encoded = HtmlEncoder.Default.Encode(malicious);
Assert.DoesNotContain("<script>", encoded);
Assert.Contains("<script>", encoded);
}
}
Important: Passing these tests does NOT mean your application is secure. Use SAST/DAST tools to find actual vulnerabilities.
Continuous Security
- CI/CD Integration - Run SAST tools in build pipeline
- Pre-commit Hooks - Scan code before commits
- Dependency Scanning - Check for vulnerable packages (
dotnet list package --vulnerable) - Penetration Testing - Periodic manual security assessments