CWE-79: 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 @variable encoding (ASP.NET Core/MVC), System.Net.WebUtility.HtmlEncode(), or Microsoft.Security.Application.Encoder for context-specific encoding. Avoid @Html.Raw() unless content is already sanitized with HtmlSanitizer library.
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");
}
Why this is vulnerable: Using @Html.Raw() or direct string concatenation bypasses ASP.NET's automatic HTML encoding, allowing user input containing <script> tags or other malicious HTML to execute in the browser.
Using HttpResponse.Write Without Encoding
// VULNERABLE
protected void Page_Load(object sender, EventArgs e)
{
string userInput = Request.QueryString["comment"];
Response.Write("<div>" + userInput + "</div>");
}
Why this is vulnerable: Response.Write() outputs raw HTML without encoding, allowing attackers to inject JavaScript or HTML tags that execute in the victim's browser when rendering the page.
Literal Controls with User Data
Why this is vulnerable: ASP.NET Literal controls render content as-is without encoding, allowing HTML and JavaScript injection when user-controlled data is included.
JavaScript Context Without Encoding
// VULNERABLE - JavaScript injection
<script>
var message = '@ViewBag.Message';
alert(message);
</script>
Why this is vulnerable: Inserting user data into JavaScript context without JavaScript-specific encoding allows attackers to break out of strings and inject arbitrary JavaScript code using quotes, comments, or other metacharacters.
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 auto-escapes HTML by default when rendering @ expressions, converting <, >, &, and quotes to entities so user input is inserted as text, not markup. This safe-by-default behavior prevents both reflected and stored XSS in MVC views without requiring developers to call an encoder manually. The engine escapes at render time, so even complex expressions like @Model.UserName or @ViewBag.Message are protected. To bypass escaping (for trusted HTML), you must explicitly use @Html.Raw(), making risky code obvious during reviews. Pair with a sanitizer (HtmlSanitizer) when allowing limited user-provided markup, and avoid @Html.Raw() on untrusted data.
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() performs HTML entity encoding server-side, converting <, >, &, and quotes into entities (<, >, &, "/') before inserting data into the response. This prevents attacker-controlled characters from breaking out of the HTML context or injecting scripts. Because encoding happens before concatenation, reflected and stored XSS are mitigated. It's part of System.Web, so no extra dependency is required for classic ASP.NET Web Forms. For newer projects, prefer HtmlEncoder.Default.Encode() from System.Text.Encodings.Web. Always encode before rendering; for JavaScript strings or URLs, use context-specific encoders (JavaScriptEncoder, UrlEncoder).
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();
}
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>
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: .NET's context-specific encoders (HtmlEncoder, JavaScriptEncoder, UrlEncoder) apply the correct escaping for each sink, preventing XSS in HTML, JavaScript, and URL contexts. HtmlEncoder converts <, >, &, and quotes to entities for element bodies and attributes. JavaScriptEncoder escapes quotes, backslashes, and control characters for JS string literals, preventing script-breaking payloads like '</script>. UrlEncoder makes query parameters safe, avoiding delimiter injection. Because each encoder is explicit and named, reviewers can spot misuse (e.g., HTML encoding inside a <script> block). Use these in ASP.NET Core controllers and views for fine-grained control; combine with Razor's auto-escaping for defense in depth.
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: The Microsoft AntiXSS library provides context-specific encoding functions for legacy .NET Framework projects that predate the built-in System.Text.Encodings.Web encoders. Encoder.HtmlEncode() escapes HTML metacharacters, JavaScriptEncode() escapes JS strings, UrlEncode() encodes URL components, and CssEncode() handles CSS contexts. These functions prevent XSS by converting attacker-controlled characters into safe equivalents before output. For new projects, prefer the built-in HtmlEncoder.Default.Encode() and related encoders in ASP.NET Core, which offer similar functionality with better performance and framework integration. Use AntiXSS only when maintaining older .NET Framework codebases that cannot upgrade.
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>
Verification
To verify XSS protection is working:
- Test with XSS payloads: Submit common XSS patterns (
<script>alert('xss')</script>,<img src=x onerror=alert('xss')>, etc.) and verify they appear encoded in HTML source - Check rendered output: View page source to confirm user input is HTML-encoded (
<appears as<,>as>) - Test JavaScript context: Verify data in JavaScript strings is properly JavaScript-encoded (quotes escaped)
- Test URL context: Confirm user data in URLs is URL-encoded
- Review Razor views: Search for
@Html.Raw(),@model.Propertywithout encoding, or JavaScript string injection points - Check Content Security Policy: Verify CSP headers are present and properly configured
- Test DOM-based XSS: Check client-side JavaScript for unsafe DOM manipulation (
innerHTML,document.writewith user data) - Use browser tools: Inspect rendered HTML and check for unencoded user input