Sanitize HTML from a rich text editor in ASP.NET Core and stop XSS
A rich text editor sends HTML to your server, and you later show that HTML to other people. If someone gets a script into it, that script runs in your visitors' browsers, on your site, with their logged-in session. This is stored cross-site scripting, or XSS, and it is the classic risk of any site with an HTML editor.
Why the browser cannot solve this
The editor cleans what people type and paste. But an attacker does not need your editor. They can post your form anything they like with a small script. Cleaning has to happen on the server, after the form arrives and before you save. And Razor's @Html.Raw(), which you need to show HTML, will not save you later.
The fix: one attribute
using RichTextBox.ModelBinding;
public class EditModel : PageModel
{
[BindProperty]
[RichTextHtml(MaxTextLength = 5000)]
public string? Body { get; set; }
public IActionResult OnPost()
{
if (!ModelState.IsValid) return Page(); // over the limit
// Body is already clean: no scripts, event handlers or javascript: links
return RedirectToPage();
}
}
<form method="post">
<richtextbox asp-for="Body" toolbar="default" height="400px"></richtextbox>
<span asp-validation-for="Body"></span>
<button type="submit">Save</button>
</form>
The HTML is cleaned during model binding, so your action never sees the dangerous version. The limit is checked on the server too, and the editor picks it up through asp-for.
What you get
We post this directly to our demo, skipping the editor:
| Posted | What the page receives |
|---|---|
<p onclick="x()">Hi</p><script>steal()</script> | The paragraph, with no handler and no script |
The model binding demo has a button that sends a hostile payload and shows both the raw value and the cleaned one.
Cleaning HTML anywhere else
The same cleaner is available as a service, for an API, a background job or old content in your database:
public class ImportService
{
private readonly IRichTextBoxHtmlSanitizer _sanitizer;
public ImportService(IRichTextBoxHtmlSanitizer sanitizer) => _sanitizer = sanitizer;
}
It is the editor's own sanitizer ported to .NET, so the browser and the server apply the same rules. Configure it through options.Sanitizer.
Showing it
<div class="article">@Html.Raw(Model.Body)</div>
Use Html.Raw only for HTML that came through the cleaner. Everything else you print, such as a user name, should use normal Razor output, which escapes it for you.