© Scott Norberg 2020
S. NorbergAdvanced ASP.NET Core 3 Security https://doi.org/10.1007/978-1-4842-6014-2_6

6. Processing User Input

Scott Norberg1 
(1)
Issaquah, WA, USA
 

At this point, I’ve covered a lot around application development security. I could keep going and cover more about securing your website – there are, after all, a whole family of attacks that target the website’s network, operating system, and even hardware that I haven’t covered. But these typically fall outside the responsibility of most software developers and so fall outside the scope of this book. But I have covered application development security pretty thoroughly, and so it’s time to move on to preventing attacks.

Fortunately for us, Microsoft has worked hard to make programming in ASP.NET safer with every version. When used as designed, Entity Framework helps prevent SQL injection attacks. Content rendering is protected from most XSS attacks. CSRF tokens are added (though not necessarily validated) by default. Unfortunately for us, though, when given a choice between adequate and superior security, the ASP.NET team pretty consistently reaches for the adequate solution. Also, it is not immediately obvious how to keep the website secure if the default functionality doesn’t fit our needs.

To learn how to successfully protect your websites, I’ll first dive into how to protect yourself from malicious input. You should be quite familiar by now with the most common attacks that can be done against your website, but may be wondering how best to protect yourself from them.

Validation Attributes

When protecting yourself from attacks, the first thing you need to do is make sure that the information coming into the system is what you expect it to be. You can prevent quite a few attacks by enforcing rules on incoming data. How is that done in .NET Core? Via attributes on your data binding models. To illustrate how these validation attributes work, I’ll create a backend for the form seen in Figure 6-1.
../images/494065_1_En_6_Chapter/494065_1_En_6_Fig1_HTML.jpg
Figure 6-1

Sample form with five fields

This form has five fields:
  • Name: Required field, but doesn’t have any specific format.

  • Email: Required, and must be in email format.

  • Word that starts with “A”: This is a word that must start with the letter “A”. (Let’s just pretend that this makes sense in this context.)

  • Age: The age must be an integer between 18 and 120.

  • Number Of Pets: This must be an integer smaller than 65,536.

How do we validate that each of these has data we expect? Let’s look at the backend. First, the Razor version.
public class SampleFormModel : PageModel
{
  [BindProperty]
  public SampleModel Model { get; set; }
  public class SampleModel
  {
    [StringLength(100)]
    [Required]
    [Display(Name = "Name")]
    public string Name { get; set; }
    [StringLength(100)]
    [Required]
    [EmailAddress]
    [Display(Name = "Email")]
    public string Email { get; set; }
    [StringLength(20)]
    [Required]
    [RegularExpression("^(a|A)(.*)")]
    [Display(Name = "Word that starts with "A"")]
    public string Word { get; set; }
    [Display(Name = "Age")]
    [IsValidAge]
    public int Age { get; set; }
    [Display(Name = "Number Of Pets")]
    public ushort PetCount { get; set; }
  }
  public void OnGet()
  {
    ViewData["Message"] = "Submit the form to test";
  }
  public void OnPost()
  {
    if (ModelState.IsValid)
      ViewData["Message"] = "Data is valid!";
    else
      ViewData["Message"] = "Please correct these errors " + ↲
                            "and try again:";
    }
  }
}
Listing 6-1

Razor Page with model validation

I’m hoping that most of the content of Listing 6-1 looks familiar to you since it is consistent with Microsoft documentation. In case it doesn’t, I’ll highlight the important parts:
  • The Required attribute tells the framework that you expect a value.

  • The EmailAddress attribute tells the framework that you expect a value in email format. There are other formats available that I’ll get to in a moment.

  • The RegularExpression attribute can come in handy whenever you want to verify that a field has a particular format, but none of the out-of-the-box options will do.

  • The StringLength attribute limits the amount of text that can be included, helping prevent a variety of attacks.

Notice that Age and PetCount also have different datatypes, int and ushort, respectively. Whenever possible, you should use a datatype to define your properties – not only is it better for readability, but it also helps limit the number of fields an attacker can use for submitting bad data. Because Age and PetCount are numbers, an attacker realistically can only submit attacks against the other three properties.

Like EmailAddress, ASP.NET has several specific format validators available. Here is a list, documentation taken from microsoft.com:1
  • [CreditCard]: Validates that the property has a credit card format

  • [Compare]: Validates that two properties in a model match

  • [EmailAddress]: Validates that the property is in email format

  • [Phone]: Validates that the property is in telephone number format

  • [Range]: Validates that the property value falls within a specified range

  • [RegularExpression]: Validates that the property value matches a specified regular expression

  • [Required]: Validates that the field is not null

  • [StringLength]: Validates that a string property value doesn’t exceed a specified length limit

  • [Url]: Validates that the property has a URL format

But what about IsValidAge? I included this because I wanted to show an example of a custom validator. In this case, letting people in younger than 18 may cause legal issues, and 120 seems like a reasonable upper limit to help prevent automated systems from entering in obviously bad values, so let’s write a validator to make sure that people are an age appropriate to use this form.
public class IsValidAge : ↲
  System.ComponentModel.DataAnnotations.ValidationAttribute
{
  protected override ValidationResult IsValid(object value,
    ValidationContext validationContext)
  {
    int age;
    if (value == null || ↲
        !int.TryParse(value.ToString(), out age))
      return new ValidationResult("Age must be a number");
    if (age < 18 || age > 120)
      return new ValidationResult(
        "You must be at least 18 years old " +
           "and younger than 120 years old");
    return ValidationResult.Success;
  }
}
Listing 6-2

Source code for custom model validator

What’s going on in Listing 6-2? This is a class that inherits from System.ComponentModel.DataAnnotations.ValidationAttribute. To make a valid attribute, all you need to do is override the IsValid method and then return a ValidationResult (with a descriptive error message if a check failed) once you’re able to determine if the check succeeds of fails.

Tip

You may also want to use the custom validator when all you need to do is a regular expression match, but you want to return a nicer error message than “The field [name] must match the regular expression [regex].”

If you’re good about putting restrictive data types on all of your data elements, you will go far in preventing many attacks. Not only will hackers need to find input that causes their attack to succeed, they will need to work around any validation you have in place. It is certainly not a cure-all, but it is a good start.

Caution

Do be careful when creating regular expression validation. You can easily create filtering that is too restrictive. As one example, you might think that you could accept only letters in the English alphabet for the first name, but you might encounter names like Žarko (like NBA player Žarko Čabarkapa), Karl-Anthony (like NBA player Karl-Anthony Towns), or D'Brickashaw (like NFL player D'Brickashaw Ferguson). What you choose to accept will depend greatly on the purpose and audience of your website.

Before we move on, please take note of the if (ModelState.IsValid) check in the OnPost method. The framework checks the validation automatically, but you have to verify the result of those checks manually. If you don’t, you could have absolutely perfect validation set up and garbage data would get in because a check for validation failure never occurred.

Caution

And no, verifying that the data is correct in JavaScript only is not sufficient. Remember how I changed the password and resubmitted the form using Burp Suite in Chapter 4? That bypassed any and all JavaScript checking. Ensuring that the input is correct in JavaScript has no real security value; it only improves the user experience for your site by providing feedback more quickly than a full POST process would.

For the sake of completeness, here’s the same code for MVC.
public class MvcController : Controller
{
  [HttpGet]
  public IActionResult SampleForm()
  {
    ViewData["Message"] = "Submit the form to test";
    return View();
  }
  [HttpPost]
  public IActionResult SampleForm(SampleModel model)
  {
    if (ModelState.IsValid)
      ViewData["Message"] = "Data is valid!";
    else
      ViewData["Message"] = "Please correct these errors " +
                            "and try again:";
    return View();
  }
}
Listing 6-3

Controller method for our sample form

There’s not much to see in Listing 6-3 since all of the validation logic is stored within the SampleModel class, which is a parameter in the POST method. So, here’s the same class built for MVC in Listing 6-4.
public class SampleModel
{
  [StringLength(100)]
  [Required]
  [Display(Name = "Name")]
  public string Name { get; set; }
  [StringLength(100)]
  [Required]
  [EmailAddress]
  [Display(Name = "Email")]
  public string Email { get; set; }
  [StringLength(20)]
  [Required]
  [RegularExpression("^(a|A)(.*)")]
  [Display(Name = "Word that starts with "A"")]
  public string Word { get; set; }
  [Display(Name = "Age")]
  [IsValidAge]
  public int Age { get; set; }
  [Display(Name = "Number Of Pets")]
  public ushort PetCount { get; set; }
}
Listing 6-4

Model for our sample MVC form

This is the same class; except this time, it isn’t a nested class within either the Controller or PageModel. Otherwise, the functionality is exactly the same between the Razor Page and the MVC version.

Validating File Uploads

What about uploading files? If we allow users to upload their own files, we need to be careful that the files themselves are safe. What are some things that you can do to check if the files are safe to use?
  • Make sure the extension matches the purpose of the upload. For instance, if you want image files, limit your upload to accepting jpg, gif, and png files only.

  • Limit the size of the file.

  • Run a virus scan on the file.

  • Check the file contents for accurate file signatures.

The first three should be fairly straightforward. The first two can be checked by looking at the file object in your server, and running a virus scan periodically should be something you can do on a regular basis. But the fourth item may require a bit of explanation. Many different file types have what’s called a file signature, or a series of bytes within the file (usually at the beginning) that is common to all files of that type. For instance, if you open a gif image, you should expect to see the file start with either “GIF87a” or “GIF89a”.2 What would a validator look like if you were to look for the signatures of common image formats? Listing 6-5 shows an example.
public class ImageFile : ValidationAttribute
{
  protected override ValidationResult IsValid(object value,
    ValidationContext validationContext)
  {
    if (!(value is IFormFile))
      return new ValidationResult("This attribute can only " +
        "be used on an IFormFile");
    byte[] fileBytes;
    var asFile = (IFormFile)value;
    using (var stream = asFile.OpenReadStream())
    {
      fileBytes = new byte[stream.Length];
      for (int i = 0; i < stream.Length; i++)
      {
        fileBytes[i] = (byte)stream.ReadByte();
      }
    }
    var ext = System.IO.Path.GetExtension(asFile.FileName);
    switch (ext)
    {
      case ".jpg":
      case ".jpeg":
      //If the first three bytes don't match the expected,
      //fail the check
        if (fileBytes[0] != 255 ||
            fileBytes[1] != 216 ||
            fileBytes[2] != 255)
          return new ValidationResult("Image appears not " +
            "to be in jpg format. Please try another.");
      //If the fourth byte doesn't match one of the four
      //expected values, fail the check
        else if (fileBytes[3] != 219 &&
                 fileBytes[3] != 224 &&
                 fileBytes[3] != 238 &&
                 fileBytes[3] != 225)
          return new ValidationResult("Image appears not " +
            "to be in jpg format. Please try another.");
        else
          //All expected bytes match
          return ValidationResult.Success;
      case ".gif":
        //If bytes 1-4 and byte 6 aren't as expected,
        //fail the check
        if (fileBytes[0] != 71 ||
            fileBytes[1] != 73 ||
            fileBytes[2] != 70 ||
            fileBytes[3] != 56 ||
            fileBytes[5] != 97)
          return new ValidationResult("Image appears not " +
            "to be in gif format. Please try another.");
        //If the fifth byte doesn't match one of the
        //expected values, fail the check
        else if (fileBytes[4] != 55 && fileBytes[4] != 57)
          return new ValidationResult("Image appears not " +
            "to be in gif format. Please try another.");
        else
          return ValidationResult.Success;
      case ".png":
        if (fileBytes[0] != 137 ||
            fileBytes[1] != 80 ||
            fileBytes[2] != 78 ||
            fileBytes[3] != 71 ||
            fileBytes[4] != 13 ||
            fileBytes[5] != 10 ||
            fileBytes[6] != 26 ||
            fileBytes[7] != 10)
          return new ValidationResult("Image appears not " +
            "to be in png format. Please try another.");
        else
          return ValidationResult.Success;
      default:
        return new ValidationResult($"Extension {ext} " +
          "is not supported. Please use gif, png, or jpg.");
    }
    //We shouldn't reach this line – add logging for the error
    throw new InvalidOperationException("Last line " +
      "reached in validating the ImageFile");
  }
}
Listing 6-5

Validator for image file signatures

You can, of course, change this method to allow for other file formats, run an antivirus checker, check file size, etc. But it’s a place to start.

Note

The code would have been more readable if I had not included the else in each case block and returned ValidationResult.Success in the last line, but in doing so, I would have been failing open. I’d recommend getting in the habit of failing closed, so the method would fail if something unexpected happens. You could easily refactor this code so you have code that looks like “if (IsValidJpg(asFile)) return ValidationResult.Success;” and make the code more readable while continuing to fail closed.

In addition to checking file contents, you should also make sure you do the following:
  • Do not use the original file name in your file system, both to prevent against various operating system attacks and also make it more difficult for a hacker to find the document if they should breach your server.

  • Do not use the original extension, just in case a script happens to get through. Instead, use an extension that the operating system won’t recognize, like “.webupload”.

  • Store the files on a server other than the webserver itself. Blob storage, either in the cloud or in a database, is likely safest. Otherwise, save the files on a separate server.

  • Consider putting your fileserver on an entirely different domain from your main web assets. For example, Twitter puts its images in the “twimg.com” domain. Not only can this help protect you if the image server is compromised, it can help with scalability if many images are uploaded and/or requested at once.

Finally, to protect yourself from files like GIFARs, you can programmatically transform files into something similar, such as transforming images into bitmaps or shrinking them by 1%.

User Input and Retrieving Files

If you do decide to store your files in the file system and you allow users to retrieve those files, you need to be extremely careful in how you get those files from your server. Many of you have seen (or maybe even coded yourself) an app that has a link to the filename, and then you get the file from the filesystem using something like this.
public class GetController : Controller
{
  IHostingEnvironment _hostEnv;
  public GetController(IHostingEnvironment hostEnv)
  {
    _hostEnv = hostEnv;
  }
  public IActionResult File(string fileName)
  {
    var path = _hostEnv.ContentRootPath + "\path\" +
      fileName;
    using (var stream = new FileStream(path, FileMode.Open))
    {
      return new FileStreamResult(stream, "application/pdf");
    }
  }
}
Listing 6-6

Code to retrieve files from the file system

But what happens with the code in Listing 6-6 if the user submits a “file” with the name “....web.config”? In this case, the user will get your configuration file. Or they can grab your app.config file with the same approach. Or, with enough patience, they may be able to steal some of your sensitive operating system files.

How do you prevent this from happening? There are two ways. The more secure way is to give users an ID, not a file name, and get the filename from a lookup of the ID. If, for whatever reason, that is absolutely not possible, you can use the Path.GetInvalidFileNameChars() method, as can be seen in Listing 6-7.
public class GetController : Controller
{
  IHostingEnvironment _hostEnv;
  public GetController(IHostingEnvironment hostEnv)
  {
    _hostEnv = hostEnv;
  }
  public IActionResult File(string fileName)
  {
    foreach (char invalid in Path.GetInvalidFileNameChars())
    {
      if (fileName.Contains(invalid))
      {
        throw new InvalidOperationException(
          $"Cannot use file names with {invalid}");
      }
    }
    var path = _hostEnv.ContentRootPath + "\path\" +
      fileName;
    using (var stream = new FileStream(path, FileMode.Open))
    {
      return new FileStreamResult(stream, "application/pdf");
    }
  }
}
Listing 6-7

Using Path.GetInvalidFileNameChars()

The same concept holds true if you’re merely reading the contents of a file. Most hackers would be just as happy seeing the contents of sensitive config or operating system files on your screen vs. getting a copy of it.

CSRF Protection

Another thing that you need to worry about when accepting user input is whether a criminal is maliciously submitting information on behalf of another. There are many things that need to be done regarding proper authentication and authorization that I’ll cover later, but in keeping with the topic of the chapter, you do need to worry about CSRF attacks. Happily for us, ASP.NET has CSRF protection that is relatively easy to implement. First, let’s protect the example from the previous section from CSRF attacks, with the code added for protection in bold in Listing 6-8.
public class MvcController : Controller
{
  [HttpGet]
  public IActionResult SampleForm()
  {
    ViewData["Message"] = "Submit the form to test";
    return View();
  }
  [ValidateAntiForgeryToken]
  [HttpPost]
  public IActionResult SampleForm(SampleModel model)
  {
    if (ModelState.IsValid)
      ViewData["Message"] = "Data is valid!";
    else
      ViewData["Message"] = "Please correct these errors " +
                            "and try again:";
    return View();
  }
}
Listing 6-8

CSRF protection in MVC

That’s it. All you need to do is add the [ValidateAntiForgeryToken] attribute to the method and ASP.NET will throw a 400 Bad Request if the token is missing. You can also tell your website to check for CSRF tokens on every POST, as seen in Listing 6-9.
public class Startup
{
  //Constructors and properties
  public void ConfigureServices(IServiceCollection services)
  {
    //Redacted
    services.AddControllersWithViews(o => o.Filters.Add(
      new AutoValidateAntiforgeryTokenAttribute()));
    services.AddRazorPages();
  }
  // public void Configure…
}
Listing 6-9

Startup.cs change to check for CSRF tokens everywhere

We don’t even have to do that much for Razor Pages – CSRF checking is done automatically there.

Note

CSRF helps protect users against attackers from submitting requests on their behalf. In other words, CSRF helps prevent the attacker from taking advantage of your users’ authentication cookies and performing an action as their victim. What about unauthenticated pages? Is there anything to protect by using CSRF checking in unauthenticated pages? The answer is “yes,” since validating CSRF tokens can serve as a prevention against someone spamming your publicly accessible form (like a Contact Me form) without doing some sort of check. But any hacker can simply make a GET, take the token and header, fill in the data, and POST their content. But since a token shouldn’t harm your user’s experience, there is not really any harm in keeping the token checking for all pages.

I hope you’re wondering at this point: how does ASP.NET’s CSRF protection work, and what exactly does it protect? After all, I talked about how the Double-Submit Cookie Pattern isn’t all that helpful. So, let’s dig further. To start, let’s take a look in Listing 6-10 at the HTML that was generated for the form in the screenshot.
<!DOCTYPE html>
<html lang="en">
<head>
  <<redacted>>
</head>
<body>
  <!-- Navigation and header removed -->
  <form method="post">
    <!-- Input fields removed for brevity -->
    <div class="form-group">
      <button type="submit" class="btn btn-primary">
        Submit Form
      </button>
    </div>
    <input name="__RequestVerificationToken" type="hidden"
      value="CfDJ8CJsmjHzXfJEiWvqrphZO5ymuIt1HTe4mgggK248YdxA
        nTDRzO3_neEvDvfbmTVBADDzBGjNnWbESzFyx3TX4wWdZwC-8fmpd
        7q-5S_837pmHid3sYaZdAkXUxcvKLaIDHepCKvZz-vU4nnjNJ27lE
        o" />
  </form>
  <!-- More irrelevant content removed -->
</body>
</html>
Listing 6-10

HTML generated for our test form (MVC)

The last input, which the framework will include for you, is the __RequestVerificationToken . This is the token that ASP.NET uses to verify the POST. Since Web is stateless, how does ASP.NET verify that this is a valid token? Listing 6-11 contains the entire POST with an authenticated user.
POST http://apressdemo.ncg/mvc/sampleform HTTP/1.1
Host: apressdemo.ncg
Proxy-Connection: keep-alive
Content-Length: 306
Cache-Control: max-age=0
Origin: http://apressdemo.ncg
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ↲
  AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117↲
  Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;↲
  q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-↲
  exchange;v=b3;q=0.9
Referer: http://apressdemo.ncg/mvc/sampleform
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: .AspNetCore.Antiforgery.9NiKlO3-_dA=CfDJ8NEghoPcg-FMm
  QdOFc5R6AfmXN_xAALvx_vLJRdFvH5ZfGF_-62X1qWcKT-ZK9FxaVDU8n31
  SwQBnGyFkoSMqr-UgJc64RuutlAvlcUd-CsQh7I8jAsLRypFZXg8iB—iOFq
  hVM8MtvGMSFHkZybNkE; .AspNetCore.Identity.Application=↲
  <<removed for brevity>>
Name=Scott+Norberg&Email=scottnorberg%40apress.com&Word=APress&Age=39&PetCount=0&__RequestVerificationToken=<<removed>>
Listing 6-11

Raw request data for form POST

So it looks like ASP.NET is using something similar to the Double-Submit Cookie Pattern, but it’s not identical. To prove it, here are the first ten characters of the request token compared to the cookie.
Table 6-1

CSRF token vs. cookie

Type

Start of Value

Token

CfDJ8CJsmj...

Cookie

CfDJ8NEgho...

Each of these starts with “CfDJ8”, but differs from there, so you know that ASP.NET is not using the Double-Submit Cookie Pattern. I’ll dig into the source code in a bit to show you what is happening, but first, I want to take you through some attacks against this functionality for two reasons. One, you can see what the token protection does (and doesn’t do) in a live-fire situation without looking at code. Two, it gives you more examples of how attacks happen.

First attack: let’s see if we can use CSRF tokens from a different user. In other words, the results from the screenshot in Figure 6-2 include authentication tokens from one user but CSRF tokens from another.
../images/494065_1_En_6_Chapter/494065_1_En_6_Fig2_HTML.jpg
Figure 6-2

CSRF attack with tokens stolen from another user

Ok, you can see from the Response on the right that I got a 400 Bad Request, indicating that the tokens are invalid. That means that I would be unable to sign up for this service, take my CSRF tokens, and then use them to attack someone else. That’s good! Now, let’s see if I can use tokens from a different site, but with the same username.
../images/494065_1_En_6_Chapter/494065_1_En_6_Fig3_HTML.jpg
Figure 6-3

CSRF attack with tokens stolen from another site

The text in Figure 6-3 might be a bit small, but I hope you can see in this screenshot that the tokens are different. I kept the authentication token the same, though, so it’s likely that there’s something about the token itself that the site doesn’t like.

Now, can we reuse tokens from one page to the next? I won’t show the screenshot for this one, but I can confirm that, yes, tokens can be reused from one page to the next.

Just to make sure I didn’t make a mistake, I tried the original tokens again.
../images/494065_1_En_6_Chapter/494065_1_En_6_Fig4_HTML.jpg
Figure 6-4

POST with original CSRF tokens

There’s good news and bad news seen in Figure 6-4. The good news is that I didn’t screw anything else up in my tests – it was the token, not some other mistake, that caused the previous screenshots to fail. The bad news? There was nothing preventing me from using the same token over again. And while I don’t have a screenshot for this, my testing yesterday proved that tokens that are 24 hours old are still valid. In short, the CSRF protection in ASP.NET is much better than the Double-Submit Cookie Pattern, but if tokens are stolen, then a hacker can use those tokens on that app on every page for that user forever.

Before we move on to fixing this problem, let’s dig into the source code a bit just to verify that these tokens are indeed specific to the user. (You don’t need to understand each line of this code, just get a general idea what it’s doing.)
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Security.Principal;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Antiforgery
{
  internal class DefaultAntiforgeryTokenGenerator :
    IAntiforgeryTokenGenerator
  {
    private readonly IClaimUidExtractor _claimUidExtractor;
    private readonly IAntiforgeryAdditionalDataProvider ↲
      _additionalDataProvider;
    public DefaultAntiforgeryTokenGenerator(
      IClaimUidExtractor claimUidExtractor,
      IAntiforgeryAdditionalDataProvider ↲
        additionalDataProvider)
    {
      _claimUidExtractor = claimUidExtractor;
      _additionalDataProvider = additionalDataProvider;
    }
    /// <inheritdoc />
    public AntiforgeryToken GenerateCookieToken()
    {
      return new AntiforgeryToken()
      {
        // SecurityToken will be populated automatically.
        IsCookieToken = true
      };
    }
    /// <inheritdoc />
    public AntiforgeryToken GenerateRequestToken(
      HttpContext httpContext,
      AntiforgeryToken cookieToken)
    {
      //Skip null reference checks for brevity
      var requestToken = new AntiforgeryToken()
      {
        SecurityToken = cookieToken.SecurityToken,
        IsCookieToken = false
      };
      var isIdentityAuthenticated = false;
      // populate Username and ClaimUid
      var authenticatedIdentity = ↲
        GetAuthenticatedIdentity(httpContext.User);
      if (authenticatedIdentity != null)
      {
        isIdentityAuthenticated = true;
        requestToken.ClaimUid = GetClaimUidBlob(↲
          _claimUidExtractor.ExtractClaimUid(↲
            httpContext.User));
        if (requestToken.ClaimUid == null)
        {
          requestToken.Username = authenticatedIdentity.Name;
        }
      }
      // populate AdditionalData
      if (_additionalDataProvider != null)
      {
        requestToken.AdditionalData = _additionalDataProvider↲
          .GetAdditionalData(httpContext);
      }
      //Code to throw exception for bad user ID removed
      return requestToken;
    }
    /// <inheritdoc />
    public bool IsCookieTokenValid(↲
      AntiforgeryToken cookieToken)
    {
      return cookieToken != null && cookieToken.IsCookieToken;
    }
    /// <inheritdoc />
    public bool TryValidateTokenSet(
      HttpContext httpContext,
      AntiforgeryToken cookieToken,
      AntiforgeryToken requestToken,
      out string message)
    {
      //Null and format checks removed
      // Is the incoming token meant for the current user?
      var currentUsername = string.Empty;
      BinaryBlob currentClaimUid = null;
      var authenticatedIdentity = ↲
        GetAuthenticatedIdentity(httpContext.User);
      if (authenticatedIdentity != null)
      {
        currentClaimUid = GetClaimUidBlob(_claimUidExtractor.↲
          ExtractClaimUid(httpContext.User));
        if (currentClaimUid == null)
        {
          currentUsername = authenticatedIdentity.Name ↲
                              ?? string.Empty;
        }
      }
      //Scheme (http vs. https) check removed
      if (!comparer.Equals(requestToken.Username, ↲
        currentUsername))
      {
        message = Resources.FormatAntiforgeryToken_↲
          UsernameMismatch(requestToken.Username,
            currentUsername);
        return false;
      }
      if (!object.Equals(requestToken.ClaimUid, ↲
        currentClaimUid))
      {
        message = Resources.AntiforgeryToken_ClaimUidMismatch;
        return false;
      }
      // Is the AdditionalData valid?
      if (_additionalDataProvider != null && ↲
          !_additionalDataProvider.ValidateAdditionalData( ↲
            httpContext, requestToken.AdditionalData))
      {
        message = Resources.AntiforgeryToken_↲
          AdditionalDataCheckFailed;
        return false;
      }
      message = null;
      return true;
    }
    private static BinaryBlob GetClaimUidBlob(string ↲
      base64ClaimUid)
    {
      //Code removed for brevity
    }
  }
}
Listing 6-12

Source code for the DefaultAntiforgeryTokenGenerator3

Listing 6-12 contains a lot of code and you don’t really need to understand every line. But there are two takeaways from this code. One, ASP.NET does indeed incorporate user ID in their CSRF tokens when possible, which should be a very effective way of preventing most CSRF attacks. To successfully pull off a CSRF attack against an ASP.NET site, an attacker would need to have, not guess or manufacture, valid tokens. Two, this code supports additional data being added to the token via the IAntiforgeryAdditionalDataProvider. I’ll explore how this can be used to minimize the harm caused by stolen tokens.

Extending Anti-CSRF Checks with IAntiforgeryAdditionalDataProvider

As long as I have the ASP.NET Core code cracked open, let’s take a look at the source for the IAntiforgeryAdditionalDataProvider interface in Listing 6-13 .4
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Antiforgery
{
  public interface IAntiforgeryAdditionalDataProvider
  {
    string GetAdditionalData(HttpContext context);
    bool ValidateAdditionalData(HttpContext context, ↲
      string additionalData);
  }
}
Listing 6-13

Source for IAntiforgeryAdditionalDataProvider

If you look carefully at the source for the DefaultAntiforgeryTokenGenerator, you should see that there isn’t support for more than one piece of additional data. Looking at the interface itself seems to confirm that it defines two methods: GetAdditionalData and ValidateAdditionalData, each of which treats “additional data” as a single string. That is a little bit of a limitation, but one we can work around. First, I’ll try to prevent stolen tokens from being valid forever. An easy way to do that is to put an expiration date on the token, as can be seen in Listing 6-14.
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http;
using System;
namespace Advanced.Security.V3.AntiCSRF
{
  public class CSRFExpirationCheck :↲
    IAntiforgeryAdditionalDataProvider
  {
    private const int EXPIRATION_MINUTES = 10;
    public string GetAdditionalData(HttpContext context)
    {
      return DateTime.Now.AddMinutes(EXPIRATION_MINUTES) ↲
        .ToString();
    }
    public bool ValidateAdditionalData(HttpContext context,
      string additionalData)
    {
      if (string.IsNullOrEmpty(additionalData))
        return false;
      DateTime toCheck;
      if (!DateTime.TryParse(additionalData, out toCheck))
        return false;
      return toCheck >= DateTime.Now;
    }
  }
}
Listing 6-14

Sample implementation of IAntiforgeryAdditionalDataProvider

Finally, you need to let the framework know that this service is available. Fortunately, this is fairly easy to do. Just add this line of code to your Startup class.
public class Startup
{
  //Constructors and properties
  public void ConfigureServices(IServiceCollection services)
  {
    //Other services
    services.AddSingleton<IAntiforgeryAdditionalDataProvider,↲
      CSRFExpirationCheck>();
  }
}
Listing 6-15

Adding our additional CSRF check to the framework’s services

With the line of code in Listing 6-15, I’m adding the CSRFExpirationCheck class to the list of services, and telling the framework that it is implementing the IAntiforgeryAdditionalDataProvider interface. Now, whenever the framework (specifically, the DefaultAntiforgeryTokenGenerator class) requests a class that implements this interface, it is the custom CSRFExpirationCheck class that will be returned.

The code for the data provider should be fairly straightforward. GetAdditionalData returns today’s date plus several minutes (I used 10 minutes in this example, anything between 5 and 60 minutes might be appropriate for your needs). ValidateAdditionalData returns true if this date is later than the date the form is actually submitted. With this code, you’d be protected from most forms of token abuse by malicious users.

This code doesn’t prevent tokens from being used multiple times, though, nor does it prevent tokens from being used on multiple pages. What are some other things that you could do to help improve the security?
  • Include the page URL within the token, then validate against context.Request.Path.

  • Include both the current page and an expiration date by separating the two with a | (pipe).

  • Include a nonce and store the nonce in a database. Once the nonce is used, reject future requests that include it.

  • Use a nonce, but in your nonce storage, include an expiration date and web path. Verify all three on each request.

For most purposes, including an expiration date should be sufficient. It provides significantly more protection than ASP.NET’s CSRF token checking does by itself while not requiring you to create your own nonce store. If you do decide to go the nonce route, you might as well include an expiration date and the current web path.

Tip

If you do decide to create and store nonces, be warned that the IAntiforgeryTokenGenerator is a Singleton service, and therefore you cannot use the Scoped Entity Framework service. You can still use database storage, of course; you will just need to find another way of getting the data to and from the database other than the EF service. Either creating a new instance of your database context or using ADO.NET should work just fine.

CSRF and AJAX

What if you want to send a POST via AJAX? If you’re creating the POST data in JavaScript, the CSRF token in the form isn’t sent back. You could include the token as form data in your POST, but that’s a bit awkward. What can you do?

It turns out that there are two places that the framework looks for this token: within “__RequestVerificationToken” in the form, or within “RequestVerificationToken” in the header. (Notice the missing double underscore for the header value.) Adding this value to the header for AJAX posts should be trivial. Your exact code will depend on the JavaScript framework you use, but Listing 6-16 shows an example using jQuery.
$.ajax({
  type: "POST",
  beforeSend: function (request) {
    request.setRequestHeader("RequestVerificationToken",
      $("[name='__RequestVerificationToken']").val());
  },
  url: some_url,
  success: function (response) {
    //Do something with the response data here
  }
});
Listing 6-16

Adding a CSRF token to a jQuery POST

Quite frankly I find this solution awkward and annoying, but it gets the job done with very little extra effort.

When CSRF Tokens Aren’t Enough

For extra sensitive operations, like password change requests or large money transactions, you may want to do more than merely protect your POST with a CSRF token. In these cases, asking the user to submit their password again helps prevent still more CSRF attacks. This action is irritating enough to your users where you won’t want to do it on every form on every page, but most will understand (and perhaps even appreciate) the extra security around the most sensitive actions.

Caution

I wouldn’t be surprised if you are thinking that if a password is needed for sensitive actions and the CSRF token can take arbitrary data, then I can include the user’s password in the CSRF token and not harm usability. My advice: do NOT do this. Not only are you not providing any extra protection against CSRF attacks, you’re potentially exposing the user’s password to hackers.

Preventing Spam

If you do go the nonce route with your CSRF tokens and turn on CSRF checking on your publicly accessible forms, you will go a long way toward preventing advertisers (and possibly malicious actors looking to cause a DoS attack) from spamming you with unwanted form submissions. (If you’ve gotten notifications for websites with any sort of Contact Me functionality, you know exactly what I’m talking about.) As I mentioned earlier, it is possible to get around this by performing a request and ensuring that any headers and tokens are returned. So, if you want to prevent even more spam, something a bit more robust is required.

One way to do this is through a CAPTCHA, or a Completely Automated Public Turing test to tell Computers and Humans Apart.5 If you’ve been on the Web, you’ve probably seen them – they’re the checks where you need to write the wavy text seen in an image, perform a simple math problem, or most annoyingly, click all of the cars, lights, signs, etc. in a 4x4 grid of images. Surprisingly, most of these CAPTCHAs are free. One of the most common, reCAPTCHA, offered by Google, is completely free and can be set up in less than an hour.6

The very old ones offered their services for free because they wanted to digitize books. They gave you two words, one to prove that you’re a human and the other to help you read text from a book to be digitized.7 It is unclear to me why the new ones are free, and “free” always makes me suspicious. The newest and most popular ones are offered by Google. Given that it’s Google, I’m guessing that they’re using the reCAPTCHA to get more data on website usage, which is a bit of a privacy risk for your users. Again, reCAPTCHA is incredibly popular, but if privacy is a concern, then perhaps a Google product shouldn’t be your first choice.

One idea I came across recently was having one or two input elements on the page that are either off-screen or otherwise invisible in some way (preferably not by making the input element itself invisible, which would be easy for a bot to find). If those hidden inputs are filled in, then you can be reasonably sure that the submission came from a bot of some kind.

Long story short, though, there is no easy, nice, and dependable way of truly reducing spam without severely affecting your users. There is no “right” answer as to how best to protect your own pages – my advice is to try different options and see what works best for you.

Mass Assignment

There’s another vulnerability that we need to talk about called mass assignment . Mass assignment is basically the term for allowing attackers to utilize hidden properties to update your database. I doubt that’s clear, so let’s dive into an example. Let’s say you have a blogging site that’s wildly successful and has thousands of bloggers. Bloggers can go into their portal, write blogs, save unfinished blogs, and then request that they get published when they’re ready. Admins then can go in and publish blogs (code not shown). The blog class looks like Listing 6-17.
public class Blog
{
  public int BlogId { get; set; }
  public string BlogTitle { get; set; }
  public string BlogContent { get; set; }
  public DateTime LastUpdated { get; set; }
  public string CreatedBy { get; set; }
  public bool IsPublished { get; set; }
}
Listing 6-17

Hypothetical blog class

In the example in Listing 6-18, the page for the user to edit the unpublished blogs might look something like this.
@model Blog
@{
    ViewData["Title"] = "Edit Unpublished Blog";
}
<h1>Edit Blog</h1>
<div class="row">
  <div class="col-md-4">
    <form method="post">
      <div asp-validation-summary="ModelOnly"
           class="text-danger"></div>
      <input type="hidden" asp-for="BlogId" />
      <div class="form-group">
        <label asp-for="BlogTitle"
               class="control-label"></label>
        <input asp-for="BlogTitle" class="form-control" />
        <span asp-validation-for="BlogTitle"
              class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="BlogContent"
               class="control-label"></label>
        <textarea asp-for="BlogContent"
                  class="form-control" />
        <span asp-validation-for="BlogContent"
              class="text-danger"></span>
      </div>
      <div class="form-group">
        <input type="submit"
               value="Save"
               class="btn btn-primary" />
      </div>
    </form>
  </div>
</div>
Listing 6-18

Hypothetical page to edit unpublished blogs

Take note of the fact that this page doesn’t include the LastUpdated, CreatedBy, or IsPublished tags because they shouldn’t be updated from this form. To save on time writing code, though, an insecure developer would reuse the Blog class as both the model and the Entity Framework object that gets saved to the database. Here’s what the controller class might look like (with necessary checks removed for the sake of brevity).
[HttpGet]
public IActionResult EditUnpublishedBlog(int? id)
{
  //Check to make sure “id” is an integer
  var blog = _context.Blog.FirstOrDefaultAsync(
                m => m.BlogId == id).Result;
  //Check to make sure that the blog exists and that the user
  //has the rights to edit it
  //Everything checks out, render the page
  return View(blog);
}
[ValidateAntiForgeryToken]
[HttpPost]
public IActionResult EditUnpublishedBlog(Blog model)
{
  //Check ModelState.IsValid
  var dbBlog = _context.Blog.FirstOrDefaultAsync(
                m => m.BlogId == model.BlogId).Result;
  //Check to make sure user has the rights to edit this blog
  //Keep the original information on who created this entry
  model.CreatedBy = dbBlog.CreatedBy;
  model.LastUpdated = DateTime.Now;
  //Skip model.IsPublished – the default is false
  //and users cannot edit published blogs anyway
  _context.Attach(model).State = EntityState.Modified;
  var result = _context.SaveChangesAsync().Result;
  return RedirectToAction("Index");
}
Listing 6-19

Hypothetical controller method to update an unpublished blog

Aside from missing checks, Listing 6-19 looks like perfectly reasonable code, doesn’t it? But in this case, the ASP.NET framework doesn’t know that you don’t want to update the IsPublished property. An attacker can open Burp and tack on “&IsPublished=true” to the end of the request and publish the blog and, in doing so, completely bypass the administrator approval process. This attack is called mass assignment.

To prevent this attack from happening, you need to make sure that your model objects contain only the properties that you want to update on that particular page. Yes, that likely means that you’ll have duplicated code, since the properties needed on one page probably won’t be exactly the same as properties on another, so you’ll have to create similar (but separate) objects for each page. But doing anything else risks attackers finding and exploiting properties unintentionally exposed via your databinding code.

To show you how this works, here is an improved version of the code in Listing 6-20.
[HttpGet]
public IActionResult EditUnpublishedBlog(int? id)
{
  //Check to make sure “id” is an integer
  var blog = _context.Blog.FirstOrDefaultAsync(
                m => m.BlogId == id &&
                m.IsPublished == false).Result;
  //Check to make sure that the blog exists and that the user
  //has the rights to edit it
  var model = new BlogModel();
  model.BlogId = blog.BlogId;
  model.BlogTitle = blog.BlogTitle;
  model.BlogContent = blog.BlogContent;
  //Everything checks out, render the page
  return View(model);
}
[ValidateAntiForgeryToken]
[HttpPost]
public IActionResult EditUnpublishedBlog(Blog model)
{
  //Check ModelState.IsValid
  var dbBlog = _context.Blog.FirstOrDefaultAsync(
                m => m.BlogId == model.BlogId).Result;
  //Check to make sure user has the rights to edit this blog
  dbBlog.BlogTitle = model.BlogTitle;
  dbBlog.BlogContent = model.BlogContent;
  dbBlog.LastUpdated = DateTime.Now;
  var result = _context.SaveChangesAsync().Result;
  return RedirectToAction("Index");
}
Listing 6-20

Controller method with security fixes

We’ve explicitly set each variable we expect to be changed instead of letting any databinding logic do it for us.

Caution

Several years ago when I was still somewhat new to MVC, I read advice from Microsoft stating that you shouldn’t use EF classes as your MVC models, but they didn’t really explain why beyond “security concerns.” So, I took their advice, but to avoid writing code that matched identical property names, I wrote a rather nifty method that would match properties from my models and automatically update my EF objects. This is only more secure if protected properties/columns don’t show up in the model objects at all, which again can change with requirements changes or refactoring. Be explicit about what you want to update. It requires more work, and it is tedious work at that, but it’s the right thing to do.

Along these same lines, you should never use your Entity Framework objects as the objects in your model. Why? Mass assignment is too easy to perform. Even if you know for sure that all properties on that page are editable, you never know when requirements change and fields get added. Will you remember to go back and fix all potential mass assignment vulnerabilities? Probably not. So, keep your models and your Entity Framework classes separate.

Tip

This goes for data you’re returning to the browser via an AJAX call, too. You’ve seen how trivially easy it is to listen to traffic using tools like Burp Suite. Any and all data you return to the browser can be seen by a user, whether it is shown in the UI or not. Try to get in the habit of only using the data you absolutely need for each and every context.

But wait, there’s more! You don’t actually have to use Burp to take advantage of this vulnerability in this situation! Because of a value shadowing vulnerability within ASP.NET, you can put that value in the query string and it’ll work, too! Just append “&IsPublished=true” to the end of your URL (assuming you have a query string, if not, use “?IsPublished=true” instead), and the ASP.NET object binding code will happily update that property for you.

This is not a good thing to say the least and another example of why value shadowing is such a dangerous thing. Thankfully there is a fix for the query string problem. If you recall from Chapter 1, ASP.NET defines several attributes that can be put on method parameters to define where they come from. To refresh your memory, here they are again:
  • FromBody: Request body

  • FromForm: Request body, but form encoded

  • FromHeader: Request header

  • FromQuery: Request query string

  • FromRoute: Request route data

  • FromServices: Request service as an action parameter

Confusingly (at least for me), FromBody and FromForm are defined as separate attributes and differ in format only. In this particular case, since we’re sending data using the name=value format of forms, FromForm is the correct one to use. Listing 6-21 contains the code with that attribute present.
[ValidateAntiForgeryToken]
[HttpPost]
public IActionResult EditUnpublishedBlog([FromForm]Blog model)
{
  //Implementation not changed
}
Listing 6-21

POST method fixed to only accept form data

In all honesty, I find these attributes annoying to code and annoying to read, but please do get in the habit of putting them in on all parameters on all controller methods. Your code will be more secure because of it.

Mass Assignment and Scaffolded Code

There’s one last thing I want to point out before going on to the next topic, and that is that you can’t trust Microsoft to give you secure options by default. You saw this with CSRF checking, you’ll see more examples later in the book, but here, let’s talk about how some scaffolded code can be vulnerable to mass assignment vulnerabilities. To help with your development, Visual Studio allows you to automatically create CRUD (Create, Retrieve, Update, and Delete) pages from Entity Framework objects. Here’s how:
  1. 1.

    Right-click your Pages folder.

     
  2. 2.

    Hover over Add.

     
  3. 3.

    Click New Scaffolded Item….

     
  4. 4.

    Click Razor Page using Entity Framework.

     
  5. 5.

    Click Add.

     
  6. 6.

    Fill out the form by adding a page name, selecting your class, selecting your data context class, and the operation you want to do (I’ll do Update in the following).

     
  7. 7.

    Click Add.

     
Once you’re done, you’ll get something that looks like this (backend only).
public class EditBlogModel : PageModel
{
  private readonly Namespace.ApplicationDbContext _context;
  public EditBlogModel(Namespace.ApplicationDbContext context)
  {
    _context = context;
  }
  [BindProperty]
  public Blog Blog { get; set; }
  public async Task<IActionResult> OnGetAsync(int? id)
  {
    if (id == null)
    {
      return NotFound();
    }
    Blog = await _context.Blog.FirstOrDefaultAsync(↲
                   m => m.BlogId == id);
    if (Blog == null)
    {
      return NotFound();
    }
    return Page();
  }
  // To protect from overposting attacks, please enable the↲
     specific properties you want to bind to, for
  // more details see https://aka.ms/RazorPagesCRUD.
  public async Task<IActionResult> OnPostAsync()
  {
    if (!ModelState.IsValid)
    {
      return Page();
    }
    _context.Attach(Blog).State = EntityState.Modified;
    try
    {
      await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
      if (!BlogExists(Blog.BlogId))
      {
        return NotFound();
      }
      else
      {
        throw;
      }
    }
    return RedirectToPage("./Index");
  }
  private bool BlogExists(int id)
  {
    return _context.Blog.Any(e => e.BlogId == id);
  }
}
Listing 6-22

Generated Update code for Entity Framework class

You should notice in Listing 6-22 that the EF class is used as a model class, which is exactly the opposite of what I said you should do. To Microsoft’s credit, they include a link in their comments (https://aka.ms/RazorPagesCRUD) that talks about mass assignment (only they call it overposting) and how to prevent it. But they probably could have created a template that created a separate model object and then manually updated the properties between the model and EF class. And then they could have added a comment saying why they didn’t use the EF class directly in the model, including this link. I really don’t understand why they didn’t. Moral of the story here, just because Microsoft does it does not mean that you should do it.

That’s about it for validating input on the way in. In the next section, I’ll talk about how to keep user input safe when displaying it on a page.

Preventing XSS

Safely displaying user-generated content has gotten easier over the years. When I first started with ASP.NET, the <asp:Label> control would happily write any text you gave it, whether that was text you intended or XSS scripts you did not. Fixing the issue wasn’t freakishly hard, but unless you knew you needed to do it, you were vulnerable to XSS (and other injection) attacks. As the years went by, the framework got better and better about preventing XSS attacks without you needing to explicitly do so yourself. There are still areas to improve, especially if you’re using a JavaScript framework of any sort.

XSS Encoding

As I mentioned earlier, there is more XSS prevention built into ASP.NET Core than in older versions of the framework. Listing 6-23 shows an example of a typical ASP.NET page, which we’ll see is not vulnerable to XSS attacks.
@{
  ViewData["Title"] = "All String In Form";
}
@model AccountUserViewModel
<h1>@ViewData["Title"]</h1>
<partial name="_Menu" />
<div class="attack-page-content">
  <!-- Instructions removed for brevity -->
  @using (Html.BeginForm())
  {
    <div>
      <label for="foodName">Search By Food Name:</label>
      <input type="text" id="foodName" name="foodName" />
    </div>
    <button type="submit">Search</button>
  }
  <h2>You searched for: @Model.SearchText</h2>
  <!-- Table to show results removed -->
</div>
Listing 6-23

Page from the Vulnerability Buffet showing user input placed on the page

Listing 6-24 shows the rendered HTML if I searched for “<script>alert(1);</script>”.
<!DOCTYPE html>
<html>
<head>
  <!-- <head> information removed for brevity -->
</head>
<body>
  <!-- <header> information removed for brevity -->
  <div class="container">
    <main role="main" class="pb-3">
      <h1>All String In Form</h1>
      <!-- <ul> menu removed for brevity -->
      <div class="attack-page-content">
        <!-- Instructions removed for brevity -->
        <form action="/sql/AllStringInForm" method="post">
          <div>
            <label for="foodName">Search By Food Name:</label>
            <input type="text" id="foodName" name="foodName"/>
          </div>
          <button type="submit">Search</button>
        </form>
        <h2>You searched for:
          &lt;script&gt;alert(1);&lt;/script&gt;</h2>
        <!-- Table removed -->
      </div>
    </main>
  </div>
  <!-- Footer and other info removed -->
</body>
</html>
Listing 6-24

Search result after an XSS attempt

Because the input is encoded, the result looks like what you see in Figure 6-5.
../images/494065_1_En_6_Chapter/494065_1_En_6_Fig5_HTML.jpg
Figure 6-5

Script shown on the page

This is great! I didn’t have to do anything at all to encode the content; ASP.NET did everything for me.

Note

Should you encode on the way in or the way out? Security professionals I respect argue either (or both), but both ASP.NET and JavaScript frameworks are clearly moving toward letting any potential XSS into the system and preventing it from being encoded as it is going out. As long as you know this and are careful rendering any user input, this is perfectly fine.

It is possible to introduce XSS vulnerabilities in ASP.NET, though. First, Listing 6-25 shows the obvious way.
@{
  ViewData["Title"] = "All String In Form";
}
@model AccountUserViewModel
<h1>@ViewData["Title"]</h1>
<partial name="_Menu" />
<div class="attack-page-content">
  <!-- Instructions removed for brevity -->
  @using (Html.BeginForm())
  {
    <div>
      <label for="foodName">Search By Food Name:</label>
      <input type="text" id="foodName" name="foodName" />
    </div>
    <button type="submit">Search</button>
  }
  <h2>You searched for: @Html.Raw(Model.SearchText)</h2>
  <!-- Table to show results removed -->
</div>
Listing 6-25

Page from the Vulnerability Buffet that is vulnerable to XSS attacks

@Html.Raw will not encode content, and as you can imagine, using it leaves you vulnerable to XSS attacks. The only time you should use this is if you trust your HTML completely, i.e., content can only come from fully trusted sources.

One source of XSS vulnerabilities that you might not think about, though, is the HtmlHelper . Listing 6-26 has an example of a way you could use the HtmlHelper to add consistent HTML for a particular need.
public static class HtmlHelperExtensionMethods
{
  public static IHtmlContent Bold(this IHtmlHelper htmlHelper,
    string content)
  {
    return new HtmlString($"<span ↲
      class='bold'>{content}</span>");
  }
}
Listing 6-26

Example of an HtmlHelper

And this can be added to a page like Listing 6-27.
@{
  ViewData["Title"] = "All String In Form";
}
@model AccountUserViewModel
<h1>@ViewData["Title"]</h1>
<partial name="_Menu" />
<div class="attack-page-content">
  <!-- Instructions removed for brevity -->
  <!-- Form removed for brevity -->
  <h2>You searched for: @Html.Bold(Model.SearchText)</h2>
  <!-- Table to show results removed -->
</div>
Listing 6-27

Page from the Vulnerability Buffet that is vulnerable to XSS attacks

Because you’re writing your own extension of the HtmlHelper, ASP.NET will not encode the content on its own. To fix the issue, you would have to do something like Listing 6-28.
public static class HtmlHelperExtensionMethods
{
  public static IHtmlContent Bold(this IHtmlHelper htmlHelper,
    string content)
  {
    var encoded = System.Net.WebUtility.HtmlEncode(content);
    return new HtmlString($"<span ↲
      class='bold'>{encoded}</span>");
  }
}
Listing 6-28

Safer example of an HtmlHelper

Instead of choosing which characters to encode, you can use the System.Net.WebUtility.HtmlEncode method to encode most of the characters you need. (System.Web.HttpUtility.HtmlEncode works too.)

Tip

In addition to encoding content, if you recall from Chapter 4, there are headers that you can put in to help stop XSS. Despite what others may think,8 most of these headers don’t do much beyond preventing some of the most obvious reflected XSS. Your site is safer with these headers configured, but they are very far from a complete solution. Remember to encode any outputs that bypass the default encoding methods.

XSS and JavaScript Frameworks

Like ASP.NET, modern JavaScript frameworks are doing a better job preventing XSS without you, as a developer, doing anything special. These are not foolproof, though, so here are a couple of tips to help you prevent XSS with your JavaScript framework:
  1. 1.

    Know whether your framework explicitly has a difference between inserting encoded text vs. HTML. For instance, jQuery has both text() and html() methods. Use the text version whenever you can.

     
  2. 2.

    Be aware of any special characters in your favorite framework, and be sure to encode those characters when rendering them on a page. For instance, Listing 6-29 shows how you could encode brackets for use with AngularJS.

     
public static class HtmlHelperExtensionMethods
{
  public static IHtmlContent AngularJSSafe(
    this IHtmlHelper htmlHelper, string content)
  {
    var encoded = System.Net.WebUtility.HtmlEncode(content);
    var safe = encoded.Replace("{", "{")
                      .Replace("}", "}");
    return new HtmlString(safe);
  }
}
Listing 6-29

HtmlHelper that encodes text for AngularJS

  1. 3.

    W96hen you set up your CSP headers, resist the temptation of creating overly permissive configurations in order to get your JavaScript framework(s) to work properly. This may be unavoidable when upgrading and/or securing a legacy app, but when creating new apps, security, including compatibility with secure CSP policies, should factor greatly into which framework you choose.

     
  2. 4.

    When in doubt, test! You can enter scripts without any special tools. I’ve shown you how to use Burp to change requests outside a browser if needed. Later on, I’ll show you how to do more general testing. But test your system for these vulnerabilities!

     

CSP Headers and Avoiding Inline Code

As long as I’m on the topic of CSP headers, it’s worth taking a minute to talk about what you’ll need to do to get the most from your CSP protection. As you have started to see, much of what attackers want to do is insert their own content on your page. It is much easier to insert content into an attribute (e.g., inline JavaScript or CSS) or by inserting an entirely new element (e.g., a new <script> tag) than it is to alter or add a new JavaScript or CSS file.

Web standards makers know this, and so they’ve included the ability for you to tell the browser, via your CSP header, to ignore all inline scripts and/or ignore all inline CSS. Listing 6-30 has a sample CSP header that allows inline scripts but specifies that all CSS should come from a file.
Content-Security-Policy: default-src 'self'; script-src 'unsafe-inline'; style-src 'self'
Listing 6-30

Sample CSP header

Since CSP headers are so hard to create, I’d strongly recommend going to https://cspisawesome.com and using their GUI to create your header.

If you need to have an inline script for whatever reason, you do have an option to safely include an inline script, and that’s to include a nonce on your <script> tag. Here’s an example.
Content-Security-Policy: default-src 'self'; script-src 'nonce-5253811ecff2'; style-src 'self'
Listing 6-31

Sample CSP header with nonce

To make the change in Listing 6-31 make a difference, you’d need to add the nonce to the script tag like Listing 6-32.
<script nonce="5253811ecff2">
  //Script content here
</script>
Listing 6-32

Script tag with nonce

Your first choice should always to keep all CSS and JavaScript in separate files. But if you’re unable to do that for whatever reason, you have other options.

In order to implement a page-specific custom CSP header, you could implement something like what’s seen in Listing 6-33.
using System;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace APressDemo.Web
{
  public class CSPNonceTestModel : PageModel
  {
    private readonly string _nonce;
    public CSPNonceTestModel()
    {
      _nonce = Guid.NewGuid().ToString().Replace("-", "");
    }
    public void OnGet()
    {
      if (Response.Headers.ContainsKey(
          "Content-Security-Policy"))
        Response.Headers.Remove("Content-Security-Policy");
      Response.Headers.Add("Content-Security-Policy",
        $"Content-Security-Policy: default-src 'self'; " +
         "script-src 'nonce-{_nonce}'; style-src 'self'");
      ViewData["Nonce"] = _nonce;
    }
  }
}
Listing 6-33

Adding a CSP header with nonce: backend

Tip

This example builds the Content Security Policy from scratch in the header. While this will work, it will be a nightmare to maintain if you have several pages that need custom CSP headers and you make frequent changes. Instead, consider a centralized CSP builder which gets altered, not built from scratch, on each page.

Here, we’re creating a new nonce in the constructor, removing any Content-Security-Policy headers if present, and then adding the nonce to the ViewData so the front end can see and use it. The front end can be seen in Listing 6-34.
@page
@model APressDemo.Web.CSPNonceTestModel
@{
  ViewData["Title"] = "CSP Nonce Test";
}
<h1>CSP Nonce Test</h1>
<p>You should see one alert for this page</p>
<script nonce="@ViewData["Nonce"]">
    alert("Nonce alert called");
</script>
<script>
    alert("Script with no nonce called");
</script>
Listing 6-34

Using the nonce on the front end

If you try this, you’ll find that only the first alert, the one in the script block, will be called in modern browsers.

Ads, Trackers, and XSS

One note for those of you who use third-party scripts to display ads, add trackers, etc.: companies can put malicious scripts in these ads. This is common enough that it has a term: malvertising.9 Many high-traffic, well-known sites have been hit with this. AOL was hit a few years ago,10 but this attack continues to be common. Aside from a reason to make sure your CSP headers are set up properly, be aware that this is a risk you need to account for when showing ads or using third-party trackers. It’s easy to sign up for such services, but you need to factor the risk of malvertising when choosing vendors.

Detecting Data Tampering

The last topic I’ll cover in this chapter is checking for data tampering. If you recall from Chapter 3, hashing is a good means of checking whether text has been changed. Doing so is fairly simple – you just need to create a new hash every time the data is changed and check the hash every time you read the data. Here is some code to help you understand how this could be done. First, let’s reuse the blog class from earlier in the chapter, but this time, let’s add a column for storing the integrity hash in Listing 6-35.
public class Blog
{
  public int BlogId { get; set; }
  public string BlogTitle { get; set; }
  public string BlogContent { get; set; }
  public string ContentHash { get; set; }
  public DateTime LastUpdated { get; set; }
  public string CreatedBy { get; set; }
  public bool IsPublished { get; set; }
}
Listing 6-35

Hypothetical blog class

And now, the class itself in Listing 6-36.
public class BlogController : Controller
{
  //For a reminder on how hashing works
  //please refer to chapter 3
  private IHasher _hasher;
  private ApplicationDbContext _context;
  public BlogController(IHasher hasher,
    ApplicationDbContext context)
  {
    _hasher = hasher;
    _context = context;
  }
  public IActionResult GetBlog(int blogId)
  {
    var blog = _context.Blog.FirstOrDefault(
                 b => b.BlogId == blogId);
    if (blog == null)
      //We’ll talk about error handling later.
      //For now, let’s just throw an exception
      throw new NullReferenceException("Blog cannot be null");
    var contentHash = _hasher.CreateHash(blog.BlogContent,
      HashAlgorithm.SHA2_512);
    if (blog.ContentHash != contentHash)
      throw new NotSupportedException("Hash does not match");
    //Reminder, we don’t want to use EF classes as models
    //Only move the properties that we need for the page
    var model = new BlogDisplayModel(blog);
    return View(blog);
  }
  public IActionResult UpdateBlog(BlogUpdateModel blog)
  {
    var dbBlog = _context.Blog.FirstOrDefault
                   (b => b.BlogId = blog.BlogId);
    //Null checks and permissions removed
    dbBlog.BlogTitle = blog.BlogTitle;
    //Other updates
    dbBlog.ContentHash = _hasher.CreateHash(blog.BlogContent,
      HashAlgorithm.SHA2_512);
    _context.SaveChanges();
    return Redirect("Somewhere");
  }
}
Listing 6-36

Pseudo-class for using hashes to detect data tampering

There isn’t much to this code. When getting the blog for display, double-check to make sure that the hash of the content matches the stored hash. This way you minimize the possibility that a hacker makes an update to your content without you knowing about it. And when you update, make sure that the hash is updated so you don’t flag changes that you make as unauthorized changes.

Summary

This was a wide-ranging chapter that covered many aspects of checking handling user input. The majority of the chapter was spent on verifying user input as it comes in by checking data types and formats, checking file contents, and retrieving files. I talked about CSRF protection and how to extend the native ASP.NET implementation. I also covered mass assignment (or overposting as Microsoft calls it). The chapter ended with a discussion about verifying data as it comes out, both in preventing XSS and in detecting data tampering.

In the next chapter, we’ll do a deep dive into how to successfully authenticate users and effectively authorize them for operations within your website. As with the CSRF checking in this chapter, I will not only go through how Microsoft wants you to use their classes, but I’ll also go over how to extend them for better security.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.23.130