flawopen.com/xss/Csharp
C# XSS: Secure Razor, Blazor and ASP.NET Core
Razor encodes by default, but Html.Raw and MarkupString bypass it. Use context-appropriate encoders and sanitize rich HTML (CWE-79).
A town permit office prints applicants’ descriptions onto official signs. Its standard Razor template treats their words as text; but stamping a description as MarkupString tells the renderer to trust it as preformatted sign layout. The browser receives the site’s text/html and interprets that markup with the site’s authority. Keep user text in Razor’s encoded path, and sanitize any rich HTML before marking it trusted.
Core Concepts & Subsystem Terms
Razor @ encoding vs Html.Raw- Razor HTML-encodes every
@expressionin.cshtmland.razorfiles.@Html.Raw(value)writes the value unencoded, andIHtmlContentvalues are trusted as already-safe markup. Both should only ever receive sanitized or constant content. Blazor MarkupString- In Blazor, a
stringrenders as text, but aMarkupStringrenders as HTML.(MarkupString)userInputornew MarkupString(userInput)inserts whatever the user typed into the component's DOM. HtmlEncoder / JavaScriptEncoderHtmlEncoder.Default.Encodeencodes for HTML element content and attributes;JavaScriptEncoder.Default.Encodeencodes for a JavaScript string literal and escapes<and'as\u003Cand\u0027. Pick the encoder that matches where the value lands.Results.Content with text/html- Minimal APIs and controllers that return
Results.Content(..., "text/html")orContent(html, "text/html")skip Razor entirely. An interpolated$"..."string in that call inserts values unencoded. HtmlSanitizer- The
Ganss.Xsspackage parses HTML with AngleSharp and keeps an allowlist of tags, attributes and URL schemes. Sanitize user-supplied rich text before wrapping it inMarkupStringor passing it toHtml.Raw.
Framework mappings
- CWE reference for the weakness described in this guide: CWE-79
- OWASP Top 10:2025 — A05:2025 Injection
Step-by-Step Attack Flow
Input reaches the endpoint
Model binding fills q, user or bio from the query string or form. A victim can be sent a crafted link, or a saved bio is rendered for every visitor.
The value skips encoding
An interpolated string in Results.Content or a MarkupString passes it through untouched. With the benign test value <img src=x onerror=alert(1)> the body contains a real tag; with ';alert(1);// the script block gains a statement.
The browser parses it
The response is served as text/html, so the browser builds a real <img> element and fires onerror, or runs the injected script statement.
The script runs as the site
The code executes with the application's origin: it can read the DOM, call the API with the victim's authentication cookie and change what the user sees.
Source Code: Flaw vs. Secure Implementation
// Program.cs: each endpoint below returns request text as HTML
using Microsoft.AspNetCore.Components;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// the interpolated string skips Razor, so q is not encoded
app.MapGet("/search", (string q) =>
Results.Content($"<h1>Results for {q}</h1>", "text/html"));
// a quote in user ends the JavaScript string literal
app.MapGet("/widget", (string user) =>
Results.Content($"<script>const user = '{user}';</script>", "text/html"));
app.Run();
public static class ProfileMarkup
{
// MarkupString renders the bio as HTML, unchanged
public static MarkupString Bio(string bio) => new MarkupString(bio);
}
// Program.cs: each value is encoded for its context or sanitized
using System.Text.Encodings.Web;
using Ganss.Xss;
using Microsoft.AspNetCore.Components;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// HtmlEncoder encodes q for HTML element content
app.MapGet("/search", (string q) =>
Results.Content($"<h1>Results for {HtmlEncoder.Default.Encode(q)}</h1>", "text/html"));
// JavaScriptEncoder escapes quotes and < for a script string
app.MapGet("/widget", (string user) =>
Results.Content($"<script>const user = '{JavaScriptEncoder.Default.Encode(user)}';</script>", "text/html"));
app.Run();
public static class ProfileMarkup
{
private static readonly HtmlSanitizer Sanitizer = new();
// HtmlSanitizer keeps an allowlist of tags before trusting the HTML
public static MarkupString Bio(string bio) => new MarkupString(Sanitizer.Sanitize(bio));
}
Engineering & System Hardening Checklist
- ✓Fail code review on
Html.Raw,new HtmlString,MarkupStringandResults.Content(..., "text/html")whose argument is not sanitized or constant; a Roslyn analyzer or Semgrep rule can enforce this in CI. - ✓Let Razor encode by default; never build HTML with string interpolation in controllers or minimal APIs.
- ✓Encode for the context with
HtmlEncoder,JavaScriptEncoderorUrlEncoder, or serialize data for scripts withJsonSerializer, which escapes<,>and&by default. - ✓Pass user-supplied HTML through
HtmlSanitizer.Sanitize()before it becomes aMarkupStringorIHtmlContent. - ✓Add a Content-Security-Policy header with a nonce-based
script-src, and keep authentication cookiesHttpOnlyandSameSite=Lax.
Sources
- CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
- Microsoft Learn — Prevent Cross-Site Scripting (XSS) in ASP.NET Core
- Microsoft Learn — Blazor: raw HTML and MarkupString
- Microsoft Learn — System.Text.Encodings.Web.JavaScriptEncoder
- HtmlSanitizer (Ganss.Xss)
- OWASP Cross Site Scripting Prevention Cheat Sheet