FlawOpen

flawopen.com/xss/Csharp

● CWE-79 · High
FlawOpen Security Research

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).

💡 Plain English Explainer (ELI5)

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 @expression in .cshtml and .razor files. @Html.Raw(value) writes the value unencoded, and IHtmlContent values are trusted as already-safe markup. Both should only ever receive sanitized or constant content.
Blazor MarkupString
In Blazor, a string renders as text, but a MarkupString renders as HTML. (MarkupString)userInput or new MarkupString(userInput) inserts whatever the user typed into the component's DOM.
HtmlEncoder / JavaScriptEncoder
HtmlEncoder.Default.Encode encodes for HTML element content and attributes; JavaScriptEncoder.Default.Encode encodes for a JavaScript string literal and escapes < and ' as \u003C and \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") or Content(html, "text/html") skip Razor entirely. An interpolated $"..." string in that call inserts values unencoded.
HtmlSanitizer
The Ganss.Xss package parses HTML with AngleSharp and keeps an allowlist of tags, attributes and URL schemes. Sanitize user-supplied rich text before wrapping it in MarkupString or passing it to Html.Raw.

Framework mappings

Step-by-Step Attack Flow

Step 1

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.

Step 2

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.

Step 3

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.

Step 4

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

UNPATCHED FLAW
// 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);
}
HARDENED SECURE PATCH
// 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

Sources