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 for ordinary view output, avoid @Html.Raw() on untrusted content, and explicitly encode with HtmlEncoder, JavaScriptEncoder, or UrlEncoder for generated HTML, JavaScript, or URL contexts. Use CSP, validation, and sanitization as supporting controls, not replacements for context-specific output encoding.
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 output, converting dangerous characters like <, >, &, and quotes into HTML entities. This prevents browsers from interpreting user input as executable markup in normal HTML body and quoted attribute contexts. The encoding happens at render time for expressions and model properties. This is the default behavior in ASP.NET Core, but JavaScript, URL, CSS, and raw HTML contexts still require context-specific handling, and @Html.Raw() explicitly bypasses the protection.
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");
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.
Remediation Steps
- Trace each finding from the untrusted source to the output sink: Razor view,
Response.Write, JavaScript block, URL, attribute, or rich HTML renderer. - Identify the exact output context and use Razor auto-encoding or the matching .NET encoder for that context.
- Replace raw output,
@Html.Raw()on user data, and manual HTML concatenation with safe Razor output or explicit encoding. - For allowed rich HTML, sanitize with an allowlist policy before storage or before rendering, and keep raw rendering locations reviewed.
- Add CSP,
X-Content-Type-Options, validation, and security analyzers after fixing the unsafe sink. - Search shared layouts, partial views, Tag Helpers, and client-side scripts for similar output patterns.
Testing
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:
Common Pitfalls
- Treating
@Html.Raw()as safe because the value came from a model or database. - Using HTML encoding in JavaScript string, URL, CSS, or JSON contexts.
- Sanitizing rich HTML once and later concatenating unsanitized fragments into it.
- Relying on CSP, validation, or security headers without fixing raw output.
- Encoding before storage and then mixing encoded and raw values across views.
- Missing DOM-based XSS in JavaScript loaded by Razor views.
Dependencies and Installation
System.Text.Encodings.Webprovides the preferredHtmlEncoder,JavaScriptEncoder, andUrlEncoderAPIs in modern .NET.System.Web.HttpUtility.HtmlEncode()and Microsoft AntiXSS are legacy options for older .NET Framework applications.Ganss.XssHtmlSanitizer can sanitize limited HTML when formatting must be preserved.- Security Code Scan, Roslyn analyzers, and DAST tools can help find missed sinks but do not replace manual review.