Data loss when passing raw HTML from View to Controller in POST request -- XSS-safety & information loss
Asked Answered
C

1

0

Preamble

My use-case includes a front-end WYSIWYG editor. Taking user input in HTML5/CSS format from the CSHTML Front-End View. Receiving the input in the Backend Controller's action via POST request. And finally doing fancy Database stuff with it.

Sounds pretty easy. Using this beast of an editor, it's very straightforward and customizable.

View

WYSIWYG editor textarea nested within a form to send editor's raw HTML data using POST

    <form class="form" asp-controller="CreationController" asp-action="CreateSnowflakeBlogpost" method="post">
        <button type="submit" class="btn btn-link">Submit Snowflake Blogpost</button>
        <textarea name="snowflakeHtmlContent" id="joditEditor"> </textarea>
    </form>

Controller

Controller's action receiving POST parameter.

    [HttpPost]
    public async Task<IActionResult> CreateSnowflakeBlogpost(string snowflakeHtmlContent)
    {
        // store HTML content in DB and do fancy operations

        // redirect to something else
        return RedirectToAction("PreviewSnowflakeBlogpost");
    }

The problem

HTML5/CSS tags are lost along the way when passing the POST data. Upon inspection, they are sent successfully from the View. The Action's parameter though has incorrect data.

Looks like sanitization is in motion here, stripping POST parameters of the HTML tags we want to deliberately keep.

It looks like there are possible solutions for this.

  • [Request.Unvalidated] annotation. Deprecated.
  • [AllowHtml] annotation. Deprecated. See here and here.
  • @Html.Raw(theString) here but it's for passing unsafe data from Controller to View. Our use-case is the opposite.
  • This question has a compilation of the previous points. All doesn't work.

The question

How do I pass my raw HTML/CSS data from View to Action? satisfying the following conditions:

  1. No data-loss of markup.

  2. Prevent unsafe data that poses XSS risk. As per the guidelines.

Callus answered 26/12, 2019 at 2:28 Comment(0)
C
1

The solution

I ended up using Custom Model Binding which bypassed this overly-eager sanitization/data loss. As a result preserved the HTML tags I want.

However this introduces XSS risk. To counter-react passing unsafe data, I used HtmlSanitizer to omit unsafe HTML/CSS tags.

Action

Added [ModelBinder(typeof(AllowSanitizedHtmlBinder))] annotation to parameter

    [HttpPost]
    public async Task<IActionResult> CreateSnowflakeBlogpost([ModelBinder(typeof(AllowSanitizedHtmlBinder))] string snowflakeHtmlContent)
    {
        // store HTML content in DB and do fancy operations

        // redirect to something else
        return RedirectToAction("PreviewSnowflakeBlogpost");
    }

Custom Model Binder

This custom model binder is like a relay and prevents any data loss in our POST parameter. HtmlSanitizer was used here before binding the value to prevent XSS.

    // Custom Model Binding
    using Microsoft.AspNetCore.Mvc.ModelBinding;

    // HTML Sanitizer
    using Ganss.XSS;

    public class AllowSanitizedHtmlBinder: IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(bindingContext));
            }

            var modelName = bindingContext.ModelName;

            // Try to fetch the value of the argument by name
            var valueProviderResult =
                bindingContext.ValueProvider.GetValue(modelName);

            if (valueProviderResult == ValueProviderResult.None)
            {
                return Task.CompletedTask;
            }

            bindingContext.ModelState.SetModelValue(modelName,
                valueProviderResult);

            var value = valueProviderResult.FirstValue;

            // Check if the argument value is null or empty
            if (string.IsNullOrEmpty(value))
            {
                return Task.CompletedTask;
            }

            // Sanitize HTML from harmful XSS markup
            var sanitizer = new HtmlSanitizer();
            var sanitizedValue = sanitizer.Sanitize(value);

            bindingContext.Result = ModelBindingResult.Success(sanitizedValue);

            return Task.CompletedTask;
        }
    }

[HELP] Missing piece -- understanding the root cause

With my working solution above, I still have no clue why HTML markup are sanitized and removed by default. Even though everyone is claiming this is not supported and such responsibility is app-specific.

As seen here and here:

You don't need [AllowHtml] anymore, because nobody denies HTML in ASP.NET Core 2.0

Don't need [AllowHtml] or RequestValidationEnabled because we don't have request validation in this system

Any help in demystifying the root cause would be HUGELY appreciated.

Sources

My solution was based on:

  1. This answer. Though request.Unvalidated is no longer supported.
  2. Custom Model Binding.
  3. HtmlSanitizer.
  4. This answer was helpful in pointing me in the right direction.
Callus answered 26/12, 2019 at 2:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.