ASP.NET Core in Action 29 Improving your application’s security

29 Improving your application’s security

This chapter covers

• Defending against cross-site scripting attacks
• Protecting from cross-site request forgery attacks
• Allowing calls to your API from other apps using CORS
• Avoiding attach vectors such as SQL injection attacks

In chapter 28 you learned how and why you should use HTTPS in your application: to protect your HTTP requests from attackers. In this chapter we look at more ways to protect your application and your application’s users from attackers. Because security is an extremely broad topic that covers lots of avenues, this chapter is by no means an exhaustive guide. It’s intended to make you aware of some of the most common threats to your app and how to counteract them, and also to highlight areas where you can inadvertently introduce vulnerabilities if you’re not careful.

TIP I strongly advise exploring additional resources around security after you’ve read this chapter. The Open Web Application Security Project (OWASP) (www.owasp.org) is an excellent resource. Alternatively, Troy Hunt has some excellent courses and workshops on security, geared toward .NET developers (https://www.troyhunt.com).

In sections 29.1 and 29.2 you’ll start by learning about two potential attacks that should be on your radar: cross-site scripting (XSS) and cross-site request forgery (CSRF). We’ll explore how the attacks work and how you can prevent them in your apps. ASP.NET Core has built-in protection against both types of attacks, but you have to remember to use the protection correctly and resist the temptation to circumvent it unless you’re certain it’s safe to do so.

Section 29.3 deals with a common scenario: you have an application that wants to use JavaScript requests to retrieve data from a second app. By default, web browsers block requests to other apps, so you need to enable cross-origin resource sharing (CORS) in your API to achieve this. We’ll look at how CORS works, how to create a CORS policy for your app, and how to apply it to specific endpoints.

The final section of this chapter, section 29.4, covers a collection of common threats to your application. Each one represents a potentially critical flaw that an attacker could use to compromise your application. The solutions to each threat are generally relatively simple; the important thing is to recognize where the flaws could exist in your own apps so you can ensure that you don’t leave yourself vulnerable.

As I mentioned in chapter 28, you should always start by adding HTTPS to your app to encrypt the traffic between your users’ browsers and your app. Without HTTPS, attackers could subvert many of the safeguards you add to your app, so it’s an important first step to take.

Unfortunately, most other security practices require rather more vigilance to ensure that you don’t accidentally introduce vulnerabilities into your app as it grows and develops. Many attacks are conceptually simple and have been known about for years, yet they’re still commonly found in new applications. In the next section we’ll look at one such attack and see how to defend against it when building apps using Razor Pages.

29.1 Defending against cross- site scripting (XSS) attacks‌

In this section I describe XSS attacks and how attackers can use them to compromise your users. I show how the Razor Pages framework protects you from these attacks, how to disable the protections when you need to, and what to look out for. I also discuss the difference between HTML encoding and JavaScript encoding, and the effect of using the wrong encoder.‌

Attackers can exploit a vulnerability in your app to create XSS attacks that execute code in another user’s browser. Commonly, attackers submit content using a legitimate approach, such as an input form, that is later rendered somewhere to the page. By carefully crafting malicious input, the attacker can execute arbitrary JavaScript on a user’s browser and so can steal cookies, impersonate the user, and generally do bad things.

TIP TIP For a detailed discussion of XSS attacks, see the “Cross Site Scripting (XSS)” article on the OWASP site: https://owasp.org/www-community/attacks/xss.

Figure 29.1 shows a basic example of an XSS attack. Legitimate users of your app can send their name to your app by submitting a form. The app then adds the name to an internal list and renders the whole list to the page. If the names are not rendered safely, a malicious user can execute JavaScript in the browser of every other user who views the list.

alt text

Figure 29.1 How an XSS vulnerability is exploited. An attacker submits malicious content to your app, which is displayed in the browsers of other users. If the app doesn’t encode the content when writing to the page, the input becomes part of the HTML of the page and can run arbitrary JavaScript.

In figure 29.1 the user entered a snippet of HTML, such as their name. When users view the list of names, the Razor template renders the names using @Html.Raw(), which writes the <script> tag directly to the document. The user’s input has become part of the page’s HTML structure. As soon as the page is loaded in a user’s browser, the<script> tag executes, and the user is compromised. Once an attacker can execute arbitrary JavaScript on a user’s browser, they can do pretty much anything.

TIP You can dramatically limit the control an attacker has even if they exploit an XSS vulnerability using a Content- Security-Policy (CSP). You can read about CSP at http://mng.bz/nWW2. I have an open-source library you can use to integrate a CSP into your app available on NuGet at http://mng.bz/vnn4.

The vulnerability here is due to rendering the user input in an unsafe way. If the data isn’t encoded to make it safe before it’s rendered, you could open your users to attack. By default, Razor protects against XSS attacks by HTML- encoding any data written using Tag Helpers, HTML Helpers, or the @ syntax. So generally you should be safe, as you saw in chapter 17.

Using @Html.Raw() is where the danger lies: if the HTML you’re rendering contains user input (even indirectly), you could have an XSS vulnerability. By rendering the user input with @ instead, the content is encoded before it’s written to the output, as shown in figure 29.2.

alt text

Figure 29.2 Protecting against XSS attacks by HTML- encoding user input using @ in Razor templates. The <script> tag is encoded so that it is no longer rendered as HTML and can’t be used to compromise your app.

This example demonstrates using HTML encoding to prevent elements being directly added to the HTML Document Object Model (DOM), but it’s not the only case you have to think about. If you’re passing untrusted data to JavaScript or using untrusted data in URL query values, you must make sure to encode the data correctly.

A common scenario is when you’re using JavaScript with Razor Pages, and you want to pass a value from the server to the client. If you use the standard @ symbol to render the data to the page, the output will be HTML-encoded.

Unfortunately, if you HTML-encode a string and inject it directly into JavaScript, you probably won’t get what you expect.

For example, if you have a variable in your Razor file called name, and you want to make it available in JavaScript, you might be tempted to use something like this:

<script>var name = '@name'</script>

If the name contains special characters, Razor will encode them using HTML encoding, which probably isn’t what you want in this JavaScript context. For example, if name was Arnold "Arnie" Schwarzenegger, rendering it as you did previously would give this:

<script>var name = 'Arnold "Arnie" Schwarzenegger';</script>

Note that the double quotation marks (") have been HTML- encoded to ". If you use this value in JavaScript directly, expecting it to be a safe encoded value, it’s going to look wrong, as shown in figure 29.3.

alt text

Figure 29.3 Comparison of alerts when using JavaScript encoding compared with HTML encoding

Instead, you should encode the variable using JavaScript encoding so that the double-quote character is rendered as a safe Unicode character, \u0022. You can achieve this by injecting a JavaScriptEncoder into the view and calling Encode() on the name variable:

@inject System.Text.Encodings.Web.JavaScriptEncoder encoder;
<script>var name = '@encoder.Encode(name)'</script>

To avoid having to remember to use JavaScript encoding, I recommend that you don’t write values into JavaScript like this. Instead, write the value to an HTML element’s attributes, and then read that into the JavaScript variable later, as shown in the following listing. That prevents the need for the JavaScript encoder entirely.

Listing 29.1 Passing values to JavaScript by writing them to HTML attributes

<div id="data" data-name="@name"></div>
<script> ❶
var ele = document.getElementById('data'); ❷
var name = ele.getAttribute('data-name'); ❸
</script>

❶ Write the value you want in JavaScript to a data- attribute. This HTML-encodes the data.
❷ Gets a reference to the HTML element
❸ Reads the data-
attribute into JavaScript, which converts it to JavaScript encoding

XSS attacks are still common, and it’s easy to expose yourself to them whenever you allow users to input data. Validation of the incoming data can help sometimes, but it’s often a tricky problem. For example, a naive name validator might require that you use only letters, which would prevent most attacks. Unfortunately, that doesn’t account for users with hyphens or apostrophes in their name, let alone users with non-Western names. People get (understandably) upset when you tell them that their name is invalid, so be wary of this approach!

Whether or not you use strict validation, you should always encode the data when you render it to the page. Think carefully whenever you find yourself writing @Html.Raw(). Is there any way, no matter how contrived, for a user to get malicious data into that field? If so, you’ll need to find another way to display the data.

XSS vulnerabilities allow attackers to execute JavaScript on a user’s browser. The next vulnerability we’re going to consider lets them make requests to your API as though they’re a different logged-in user, even when the user isn’t using your app. Scared? I hope so!‌

29.2 Protecting from cross-site request forgery (CSRF) attacks‌

In this section you’ll learn about CSRF attacks, how attackers can use them to impersonate a user on your site, and how to protect against them using antiforgery tokens. Razor Pages protects you from these attacks by default, but you can disable these verifications, so it’s important to understand the implications of doing so.

CSRF attacks can be a problem for websites or APIs that use cookies for authentication. A CSRF attack involves a malicious website making an authenticated request to your API on behalf of the user, without the user’s initiating the request. In this section we’ll explore how these attacks work and how you can mitigate them with antiforgery tokens.

The canonical example of this attack is a bank transfer/withdrawal. Imagine you have a banking application that stores authentication tokens in a cookie, as is common (especially in traditional server-side rendered applications).Browsers automatically send the cookies associated with a domain with every request so the app knows whether a user is authenticated.

Now imagine your application has a page that lets a user transfer funds from their account to another account using a POST request to the Balance Razor Page. You have to be logged in to access the form (you’ve protected the Razor Page with the [Authorize] attribute or global authorization requirements), but otherwise you post a form that says how much you want to transfer and where you want to transfer it. Seems simple enough?‌

Suppose that a user visits your site, logs in, and performs a transaction. Then they visit a second website that the attacker has control of. The attacker has embedded a form in their website that performs a POST to your bank’s website, identical to the transfer-funds form on your banking website. This form does something malicious, such as transfer all the user’s funds to the attacker, as shown in figure 29.4.

Browsers automatically send the cookies for the application when the page does a full form post, and the banking app has no way of knowing that this is a malicious request. The unsuspecting user has given all their money to the attacker!

alt text

Figure 29.4 A CSRF attack occurs when a logged-in user visits a malicious site. The malicious site crafts a form that matches one on your app and POSTs it to your app. The browser sends the authentication cookie automatically, so your app sees the request as a valid request from the user.

The vulnerability here revolves around the fact that browsers automatically send cookies when a page is requested (using a GET request) or a form is POSTed. There’s no difference between a legitimate POST of the form in your banking app and the attacker’s malicious POST. Unfortunately, this behavior is baked into the web; it’s what allows you to navigate websites seamlessly after initially logging in.

TIP Browsers have additional protections to prevent cookies being sent in this situation, called SameSite cookies. By default, most browsers use SameSite=Lax, which prevents this vulnerable behavior. You can read about SameSite cookies and how to work with them in ASP.NET Core at http://mng.bz/4DDj.

A common solution to this CSRF attack is the synchronizer token pattern, which uses user-specific, unique antiforgery tokens to enforce a difference between a legitimate POST and a forged POST from an attacker. One token is stored in a cookie, and another is added to the form you wish to protect. Your app generates the tokens at runtime based on the current logged-in user, so there’s no way for an attacker to create one for their forged form.

TIP The “Cross-Site Request Forgery Prevention Cheat Sheet” article on the OWASP site (http://mng.bz/5jRa) has a thorough discussion of the CSRF vulnerability, including the synchronizer token pattern.

When the Balance Razor Page receives a form POST, it compares the value in the form with the value in the cookie. If either value is missing or the values don’t match, the request is rejected. If an attacker creates a POST, the browser posts the cookie token as usual, but there won’t be a token in the form itself or the token won’t be valid. The Razor Page rejects the request, protecting from the CSRF attack, as in figure 29.5.

alt text

Figure 29.5 Protecting against a CSRF attack using antiforgery tokens. The browser automatically forwards the cookie token, but the malicious site can’t read it and so can’t include a token in the form.The app rejects the malicious request because the tokens don’t match.

The good news is that Razor Pages automatically protects you against CSRF attacks. The Form Tag Helper automatically sets an antiforgery token cookie and renders the token to a hidden field called _RequestVerificationToken for every <form> element in your app (unless you specifically disable them). For example, take this simple Razor template that posts back to the same Razor Page:

<form method="post">
<label>Amount</label>
<input type="number" name="amount" />
<button type="submit">Withdraw funds</button>
</form>

When rendered to HTML, the antiforgery token is stored in the hidden field and is posted back with a legitimate request:

<form method="post">
<label>Amount</label>
<input type="number" name="amount" />
<button type="submit" >Withdraw funds</button>
<input name="__RequestVerificationToken" type="hidden"

value="CfDJ8Daz26qb0hBGsw7QCK"/>

</form>

ASP.NET Core automatically adds the antiforgery tokens to every form, and Razor Pages automatically validates them. The framework ensures that the antiforgery tokens exist in both the cookie and the form data, ensures that they match, and rejects any requests where they don’t.

If you’re using Model-View-Controller (MVC) controllers with views instead of Razor Pages, ASP.NET Core still adds the antiforgery tokens to every form. Unfortunately, it doesn’t validate them for you. Instead, you must decorate your controllers and actions with the [ValidateAntiForgeryToken] attribute. This ensures that the antiforgery tokens exist in both the cookie and the form data, checks that they match, and rejects any requests in which they don’t.

WARNING ASP.NET Core doesn’t automatically validate antiforgery tokens if you’re using MVC controllers with Views. You must make sure to mark all vulnerable methods with [ValidateAntiForgeryToken] attributes instead, as described in the “Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core” documentation: http://mng.bz/QPPv. Note that if you’re not using cookies for authentication, you are not vulnerable to CSRF attacks: CSRF attacks arise from attackers exploiting the fact that browsers automatically attach cookies to requests. No cookies, no problem!

Generally, you need to use antiforgery tokens only for POST, DELETE, and other dangerous request types that are used for modifying state. GET requests shouldn’t be used for this purpose, so the framework doesn’t require valid antiforgery tokens to call them. Razor Pages validates antiforgery tokens for dangerous verbs like POST and ignores safe verbs like GET. As long as you create your app following this pattern‌‌ (and you should!), the framework does the right thing to keep you safe.

If you need to explicitly ignore antiforgery tokens on a Razor Page for some reason, you can disable the validation by applying the [IgnoreAntiforgeryToken] attribute to a Razor Page’s PageModel. This bypasses the framework protections for those cases when you’re doing something that you know is safe and doesn’t need protecting, but in most cases it’s better to play it safe and validate.‌

CSRF attacks can be a tricky thing to get your head around from a technical point of view, but for the most part everything should work without much effort on your part.

Razor adds antiforgery tokens to your forms, and the Razor Pages framework takes care of validation for you.

Things get trickier if you’re making a lot of requests to an API using JavaScript, and you’re posting JavaScript Object Notation (JSON) objects rather than form data. In these cases, you won’t be able to send the verification token as part of a form (because you’re sending JSON), so you’ll need to add it as a header in the request instead. Microsoft’s documentation “Prevent Cross-Site Request Forgery (XSRF/ CSRF) attacks in ASP.NET Core” contains an example of adding the header in JavaScript and validating it in your application. See http://mng.bz/XNNa.‌

TIP If you’re not using cookie authentication and instead have a single-page application (SPA) that sends authentication tokens in a header, the good news is that you don’t have to worry about CSRF at all! Malicious sites can send only cookies, not headers, to your API, so they can’t make authenticated requests.

Generating unique tokens with the data protection APIs

The antiforgery tokens used to prevent CSRF attacks rely on the ability of the framework to use strong symmetric encryption to encrypt and decrypt data. Encryption algorithms typically rely on one or more keys, which are used to initialize the encryption and to make the process reproducible. If you have the key, you can encrypt and decrypt data; without it, the data is secure.

In ASP.NET Core, encryption is handled by the data protection APIs. They’re used to create the antiforgery tokens, encrypt authentication cookies, and generate secure tokens in general. Crucially, they also control the management of the key files that are used for encryption. A key file is a small XML file that contains the random key value used for encryption in ASP.NET Core apps. It’s critical that it’s stored securely. If an attacker got hold of it, they could impersonate any user of your app and generally do bad things!

The data protection system stores the keys in a safe location, depending on how and where you host your app:

• Azure Web App—In a special synced folder, shared between regions

• IIS without user profile—Encrypted in the registry

• Account with user profile—In %LOCALAPPDATA%\ASP.NET\DataProtection-Keys on Windows, or ~/.aspnet/DataProtection-Keys on Linux or macOS

• All other cases—In memory; when the app restarts, the keys will be lost

So why do you care? For your app to be able to read your users’ authentication cookies, it must decrypt them by using the same key that was used to encrypt them. If you’re running in a web-farm scenario, by default each server has its own key and won’t be able to read cookies encrypted by other servers.

To get around this, you must configure your app to store its data protection keys in a central location. This could be a shared folder on a hard drive, a Redis instance, or an Azure blob storage instance, for example.

Microsoft’s documentation on the data protection APIs is extremely detailed, but it can be overwhelming. I recommend reading the section on configuring data protection, (“Configure ASP.NET Core Data Protection,” http://mng.bz/d40i) and configuring a key storage provider for use in a web- farm scenario (“Key storage providers in ASP.NET Core,” http://mng.bz/5pW6). I also have an introduction to the data protection APIs on my blog at http://mng.bz/yQQd.

It’s worth clarifying that the CSRF vulnerability discussed in this section requires that a malicious site does a full form POST to your app. The malicious site can’t make the request to your API using client-side-only JavaScript, as browsers block JavaScript requests to your API that are from a different origin.

This is a safety feature, but it can often cause you problems. If you’re building a client-side SPA, or even if you have a little JavaScript on an otherwise server-side rendered app, you may need to make such cross-origin requests. In the next section I describe a common scenario you’re likely to run into and show how you can modify your apps to work around Pit.

29.3 Calling your web APIs from other domains using CORS‌

In this section you’ll learn about cross-origin resource sharing (CORS), a protocol to allow JavaScript to make requests from one domain to another. CORS is a frequent area of confusion for many developers, so this section describes why it’s necessary and how CORS headers work. You’ll then learn how to add CORS to both your whole application and specific web API actions, and how to configure multiple CORS policies for your application.

As you’ve already seen, CSRF attacks can be powerful, but they would be even more dangerous if it weren’t for browsers implementing the same-origin policy. This policy blocks apps from using JavaScript to call a web API at a different location unless the web API explicitly allows it.

DEFINITION Origins are deemed to be the same if they match the scheme (HTTP or HTTPS), domain (example.com), and port (80 by default for HTTP and 443 for HTTPS). If an app attempts to access a resource using JavaScript, and the origins aren’t identical, the browser blocks the request.

The same-origin policy is strict. The origins of the two URLs must be identical for the request to be allowed. For example, the following origins are the same:

http://example.com/home
http://example.com/site.css

The paths are different for these two URLs (/home and /site.css), but the scheme, domain, and port (80) are identical. So if you were on the home page of your app, you could request the /site.css file using JavaScript without any problems.

By contrast, the origins of the following sites are different, so you couldn’t request any of these URLs using JavaScript from the http://example.com origin:

https://example.com—Different scheme (https)

http://www.example.com—Different domain (includes a subdomain)

http://example.com:5000—Different port (default HTTP port is 80)

For simple apps, where you have a single web app handling all your functionality, this limitation might not be a problem, but it’s extremely common for an app to make requests to another domain. For example, you might have an e- commerce site hosted at http://shopping.com, and you’re attempting to load data from http://api.shop ping.com to display details about the products available for sale. With this configuration, you’ll fall foul of the same-origin policy.Any attempt to make a request using JavaScript to the API domain will fail, with an error similar to figure 29.6.

alt text

Figure 29.6 The console log for a failed cross-origin request. Chrome has blocked a cross-origin request from the app http://shopping.com:6333 to the API at http://api.shopping.com:5111.

The need to make cross-origin requests from JavaScript is increasingly common with the rise of client-side SPAs and the move away from monolithic apps. Luckily, there’s a web standard that lets you work around this in a safe way; this standard is CORS. You can use CORS to control which apps can call your API, so you can enable scenarios like this one.

29.3.1 Understanding CORS and how it works‌

CORS is a web standard that allows your web API to make statements about who can make cross-origin requests to it. For example, you could make statements such as these:

• Allow cross-origin requests from https://shopping.com and https://app.shopping.com.

• Allow only GET cross-origin requests.

• Allow returning the Server header in responses to cross-origin requests.

• Allow credentials (such as authentication cookies or authorization headers) to be sent with cross- origin requests.

You can combine these rules into a policy and apply different policies to different endpoints of your API. You could apply a policy to your entire application or a different policy to every API action.

CORS works using HTTP headers. When your web API application receives a request, it sets special headers on the response to indicate whether cross-origin requests are allowed, which origins they’re allowed from, and which HTTP verbs and headers the request can use—pretty much everything about the request.

In some cases, before sending a real request to your API, the browser sends a preflight request, a request sent using the OPTIONS verb, which the browser uses to check whether it’s allowed to make the real request. If the API sends back the correct headers, the browser sends the true cross-origin request, as shown in figure 29.7.‌

alt text

Figure 29.7 Two cross-origin requests. The response to the GET request doesn’t contain any CORS headers, so the browser blocks the app from reading it, even though the response may contain data from the server. The second request requires a preflight OPTIONS request to check whether CORS is enabled. As the response contains CORS headers, the browser makes the real request and provides the response to the JavaScript app.

TIP For a more detailed discussion of CORS, see CORS in Action, by Monsur Hossain (Manning, 2014), http://mng.bz/aD41.‌

The CORS specification, which you can find at http://mng.bz/MBBB, is complicated, with a variety of headers, processes, and terminology to contend with. Fortunately, ASP.NET Core handles the details of the specification for you, so your main concern is working out exactly who needs to access your API, and under what circumstances.

29.3.2 Adding a global CORS policy to your whole app‌

Typically, you shouldn’t set up CORS for your APIs until you need it. Browsers block cross-origin communication for a reason: it closes an avenue of attack. They’re not being awkward. Wait until you have an API hosted on a different domain to the app that needs to access it.

Adding CORS support to your application requires you to do four things:

• Add the CORS services to your app.

• Configure at least one CORS policy.

• Add the CORS middleware to your middleware pipeline.

• Set a default CORS policy for your entire app or decorate your endpoints with EnableCors metadata to selectively enable CORS for specific endpoints.

To add the CORS services to your application, call AddCors() on your WebApplicationBuilder instance in Program.cs:

builder.Services.AddCors();

The bulk of your effort in configuring CORS will go into policy configuration. A CORS policy controls how your application responds to cross-origin requests. It defines which origins are allowed, which headers to return, which HTTP methods to allow, and so on. You normally define your policies inline when you add the CORS services to your application.

Consider the previous e-commerce site example. You want your API that is hosted at http://api.shopping.com to be available from the main app via client-side JavaScript, hosted at http://shopping.com. You therefore need to configure the API to allow cross-origin requests.

NOTE Remember, it’s the app that will get errors when attempting to make cross-origin requests, but it’s the API you’re accessing that you need to add CORS to, not the app making the requests.

The following listing shows how to configure a policy called "AllowShoppingApp" to enable cross-origin requests from http://shopping.com to the API. Additionally, we explicitly allow any HTTP verb type; without this call, only simple methods (GET, HEAD, and POST) are allowed. The policies are built up using the familiar fluent builder style you’ve seen throughout this book.

Listing 29.2 Configuring a CORS policy to allow requests from a specific origin

public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options => { ❶
options.AddPolicy("AllowShoppingApp", policy => ❷
policy.WithOrigins("http://shopping.com") ❸
.AllowAnyMethod()); ❹
});
// other service configuration
}

❶ The AddCors method exposes an Action overload.
❷ Every policy has a unique name.
❸ The WithOrigins method specifies which origins are allowed. Note that the URL has no trailing /.
❹ Allows all HTTP verbs to call the API

WARNING When listing origins in WithOrigins(), ensure that they don’t have a trailing "/"; otherwise, the origin will never match, and your cross-origin requests will fail.

Once you’ve defined a CORS policy, you can apply it to your application. In the following listing, you apply the "AllowShoppingApp" policy to the whole application using CorsMiddleware by calling UseCors().

Listing 29.3 Adding the CORS middleware and configuring a default CORS policy

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options => {
options.AddPolicy("AllowShoppingApp", policy =>
policy.WithOrigins("http://shopping.com")
.AllowAnyMethod());
});
var app = builder.Build();
app.UseRouting();
app.UseCors("AllowShoppingApp"); ❶
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/api/products", () => new string[] {});
app.Run();

❶ Adds the CORS middleware and uses AllowShoppingApp as the default policy

❶ Adds the CORS middleware and uses AllowShoppingApp as the default policy

NOTE As with all middleware, the order of the CORS middleware is important. You must place the call to UseCors() after UseRouting(). The CORS middleware needs to intercept cross-origin requests to your web API actions so it can generate the correct responses to preflight requests and add the necessary headers. It’s common to place the CORS middleware before a call to UseAuthentication().

With the CORS middleware in place for the API, the shopping app can now make cross-origin requests. You can call the API from the http://shopping.com site, and the browser lets the CORS request through, as shown in figure 29.8. If you make the same request from a domain other than http://shopping.com, the request continues to be blocked.

alt text

Figure 29.8 With CORS enabled, as in the bottom image, cross-origin requests can be made, and the browser will make the response available to the JavaScript. Compare this to the top image, in which the request was blocked.

Applying a CORS policy globally to your application in this way may be overkill. If there’s only a subset of actions in your API that need to be accessed from other origins, it’s prudent to enable CORS only for those specific actions. This can be achieved by adding metadata to your endpoints.

29.3.3 Adding CORS to specific endpoints with EnableCors metadata‌

Browsers block cross-origin requests by default for good reason: they have the potential to be abused by malicious or compromised sites. Enabling CORS for your entire app may not be worth the risk if you know that only a subset of actions will ever need to be accessed cross-origin.

If that’s the case, it’s best to enable a CORS policy only for those specific endpoints. ASP.NET Core provides the RequireCors() method, which you can apply to your minimal API endpoints or route groups, and the [EnableCors] attribute, which lets you select a policy to apply to a given controller or action method.

NOTE Both these methods add CORS metadata to the endpoint, which is used by the CorsMiddleware to determine the policy to apply. This is why the CorsMiddleware should be placed after the RoutingMiddleware, so that the CorsMiddleware knows which endpoint was selected and so which CORS policy to apply.

With the RequireCors() method and [EnableCors] attribute, you can apply different CORS policies to different endpoints. For example, you could allow GET requests access to your entire API from the http://shopping.com domain but‌ allow other HTTP verbs only for a specific endpoint while allowing anyone to access your product list endpoints.

You define CORS policies in the call to AddCors() by calling AddPolicy() and giving the policy a name, as you saw in listing 29.2. If you’re using endpoint-specific policies, instead of calling UseCors("AllowShoppingApp") as you saw in listing 29.3, you should add the middleware without a default policy by calling UseCors() only.

You can then selectively enable CORS for individual endpoints and specifying the policy to apply. To apply CORS to a minimal API endpoint or route group, call RequireCors("AllowShoppingApp"), as shown in the following listing. To apply a policy to a controller or an action method, apply the [EnableCors("AllowShoppingApp"] attribute. You can disable cross-origin access for an endpoint by applying the [DisableCors] attribute.

Listing 29.4 Applying a CORS policy to minimal API endpoints

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options => { /* Config not shown*/});
var app = builder.Build();
app.UseCors(); ❶
app.MapGet("/api/products", () => new string[] {})
.RequireCors("AllowShoppingApp"); ❷
app.MapGet("/api/products",
[EnableCors("AllowShoppingApp")] () => new { }); ❸
app.MapGroup("/api/categories")
.RequireCors("AllowAnyOrigin"); ❹
app.MapDelete("/api/products",
[DisableCors] () => Results.NoContent()); ❺
app.Run();

❶ Adds the CorsMiddleware without configuring a default policy
❷ Applies the AllowShoppingApp CORS policy to the endpoint
❸ You can apply attributes to the lamba or handler method, as well as to MVC action methods.
❹ You can apply CORS policies to whole route groups.
❺ The DisableCors attribute disables CORS for the endpoint completely.

If you define a default policy but then also call RequireCors() or add an [EnableCors] attribute, then both policies are applied. This can get confusing, so I recommend not applying a default CORS policy in the middleware and specifying the policy at the route group or endpoint level. Alternatively, if you do want to apply a policy to your whole app, avoid applying individual policies to endpoints as well.

Whether you choose to use a single default CORS policy or multiple policies, you need to configure the CORS policies for your application in the call to AddCors. Many options are available when configuring CORS. In the next section I provide an overview of the possibilities.

29.3.4 Configuring CORS policies‌

Browsers implement the cross-origin policy for security reasons, so you should carefully consider the implications of relaxing any of the restrictions they impose. Even if you enable cross-origin requests, you can still control what data cross-origin requests can send and what your API returns. For example, you can configure

• The origins that may make a cross-origin request to your API

• The HTTP verbs (such as GET, POST, and DELETE) that can be used

• The headers the browser can send

• The headers the browser can read from your app’s response

• Whether the browser will send authentication credentials with the request

You define all these options when creating a CORS policy in your call to AddCors() using the CorsPolicyBuilder, as you saw in listing 29.2. A policy can set all or none of these options, so you can customize the results to your heart’s content. Table 29.1 shows some of the options available and their effects.

Table 29.1 The methods available for configuring a CORS policy and their effect on the policy

CorsPolicyBuilder method example Result
WithOrigins("http://shopping.com") Allows cross-origin requests from http:/ /shopping.com
AllowAnyOrigin() Allows cross-origin requests from any origin. This means any website can make JavaScript requests to your API.
WithMethods()/AllowAnyMethod() Sets the allowed methods (such as GET, POST, and DELETE) that can be made to your API
WithHeaders()/AllowAnyHeader() Sets the headers that the browser may send to your API. If you restrict the headers, you must include at least Accept, Content-Type, and Origin to allow valid requests.
WithExposedHeaders() Allows your API to send extra headers to the browser. By default, only the Cache-Control, Content-Language,Content-Type, Expires, Last-Modified,and Pragma headers are sent in the response.
AllowCredentials() By default, the browser won’t send authentication details with cross- origin requests unless you explicitly allow it. You must also enable sending credentials client-side in JavaScript when making the request.

One of the first problems in setting up CORS is realizing you have a cross-origin problem at all. Several times I’ve been stumped trying to figure out why a request won’t work, until I realize the request is going cross-domain or from HTTP to HTTPS, for example.

Whenever possible, I recommend avoiding cross-origin requests. You can end up with subtle differences in the way browsers handle them, which can cause more headaches. In particular, avoid HTTP to HTTPS cross-domain problems by running all your applications behind HTTPS. As discussed in chapter 28, that’s a best practice anyway, and it’ll help prevent a whole class of CORS headaches.

TIP Another (often preferable) option is to configure CORS policies in your reverse proxy or application gateway. You can configure Azure App Service with allowed origins, for example, so that you don’t need to modify your application code.

Once I’ve established that I definitely need a CORS policy, I typically start with the WithOrigins() method. Then I expand or restrict the policy further, as need be, to provide cross-origin lockdown of my API while still allowing the required functionality. CORS can be tricky to work around, but remember, the restrictions are there for your safety.

Cross-origin requests are only one of many potential avenues attackers could use to compromise your app. Many of these are trivial to defend against, but you need to be aware of them and know how to mitigate them. In the next section we’ll look at common threats and how to avoid them.

29.4 Exploring other attack vectors‌

So far in this chapter, I’ve described two potential ways attackers can compromise your apps—XSS and CSRF attacks

—and how to prevent them. Both of these vulnerabilities regularly appear in the OWASP top ten list of most critical web app risks, so it’s important to be aware of them and to avoid introducing them into your apps.

TIP OWASP publishes the list online, with descriptions of each attack and how to prevent those attacks. There’s a cheat sheet for staying safe here: https://cheatsheetseries.owasp.org.

In this section I’ll provide an overview of some of the other most common vulnerabilities and how to avoid them in your apps.

29.4.1 Detecting and avoiding open redirect attacks‌

A common OWASP vulnerability is due to open redirect attacks. An open redirect attack occurs when a user clicks a link to an otherwise-safe app and ends up being redirected to a malicious website, such as one that serves malware. The safe app contains no direct links to the malicious website, so how does this happen?

Open redirect attacks occur where the next page is passed as a parameter to an endpoint. The most common example is when you’re logging in to an app. Typically, apps remember the page a user is on before redirecting them to a login page by passing the current page as a returnUrl query string parameter. After the user logs in, the app redirects the user to the returnUrl to carry on where they left off.

Imagine a user is browsing an e-commerce site. They click Buy for a product and are redirected to the login page. The product page they were on is passed as the returnUrl, so after they log in, they’re redirected to the product page instead of being dumped back to the home screen.

An open redirect attack takes advantage of this common pattern, as shown in figure 29.9. A malicious attacker creates a login URL where the returnUrl is set to the website they want to send the user to and convinces the user to click the link to your web app. After the user logs in, a vulnerable app redirects the user to the malicious site.

alt text

Figure 29.9 An open redirect makes use of the common return URL pattern. This is typically used for login pages but may be used in other areas of your app too. If your app doesn’t verify that the URL is safe before redirecting the user, it could redirect users to malicious sites.

The simple solution to this attack is to always validate that the returnUrl is a local URL that belongs to your app before redirecting users to it. The default Identity UI does this already, so you shouldn’t have to worry about the login page if you’re using Identity, as described in chapter 23.

If you have redirects in other parts of your app, ASP.NET Core provides a couple of helper methods for staying safe, the most useful of which is Url.IsLocalUrl(). Listing 29.5 shows how you could verify that a provided return URL is safe and, if not, redirect to the app’s home page.

You can also use the LocalRedirect() helper method on the ControllerBase and Razor Page PageModel classes, which throw an exception if the provided URL isn’t local.‌‌

Listing 29.5 Detecting open redirect attacks by checking for local return URLs

[HttpPost]
public async Task<IActionResult> Login(
LoginViewModel model, string returnUrl = null) ❶
{
// Verify password, and sign user in
if (Url.IsLocalUrl(returnUrl)) ❷
{
return Redirect(returnUrl); ❸
}
else
{
return RedirectToPage("Index"); ❹
}
}

❶ The return URL is provided as an argument to the action method.
❷ Returns true if the return URL starts with / or ~/
❸ The URL is local, so it’s safe to redirect to it.
❹ The URL was not local and could be an open redirect attack, so redirect to the homepage for safety.

This simple pattern protects against open redirect attacks that could otherwise expose your users to malicious content. Whenever you’re redirecting to a URL that comes from a query string or other user input, you should use this pattern.

TIP In some authentication flows, such as when authenticating with OpenID Connect, you can’t redirect to a local URL, so you can’t use this pattern. Instead, OpenID Connect requires that you preregister the allowed redirect URLs and redirect only to a registered URL. You should consider using this pattern when you can’t enforce a local- only redirect.

Open redirect attacks present a risk to your users rather than to your app directly. The next vulnerability represents a critical vulnerability in your app itself.

29.4.2 Avoiding SQL injection attacks with EF Core and parameterization‌

SQL injection attacks represent one of the most dangerous threats to your application. Attackers craft simple malicious input, which they send to your application as traditional form-based input or by customizing URLs and query strings to execute arbitrary code against your database. An SQL injection vulnerability could expose your entire database to attackers, so it’s critical that you spot and remove any such vulnerabilities in your apps.

I hope I’ve scared you a little with that introduction, so now for the good news: if you’re using Entity Framework Core (EF Core) or pretty much any other object-relational mapper (ORM) in a standard way, you should be safe. EF Core has built-in protections against SQL injection, so as long as you’re not doing anything funky, you should be fine.

SQL injection vulnerabilities occur when you build SQL statements yourself and include dynamic input that an attacker provides, even indirectly. EF Core provides the ability to create raw SQL queries using the FromSqlRaw() method, so you must be careful when using this method.

Imagine your recipe app has a search form that lets you search for a recipe by name. If you write the query using LINQ extension methods (as discussed in chapter 12), you would have no risk of SQL injection attacks. However, if you decide to write your SQL query by hand, you open yourself to such a vulnerability, as shown in the following listing.

Listing 29.6 An SQL injection vulnerability in EF Core due to string concatenation

public IList<User> FindRecipe(string search) ❶
{
return _context.Recipes ❷
.FromSqlRaw("SELECT * FROM Recipes" + ❸
"WHERE Name = '" + search + "'") ❹
.ToList();
}

❶ The search parameter comes from user input, so it’s unsafe.
❷ The current EF Core DbContext is held in the _context field.
❸ You can write queries by hand using the FromSqlRaw extension method.
❹ This introduces the vulnerability—including unsafe content directly in an SQL
string.

In this listing, the user input held in search is included directly in the SQL query. By crafting malicious input, users can potentially perform any operation on your database.

Imagine an attacker searches your website using the text

'; DROP TABLE Recipes; --

Your app assigns this to the search parameter, and the SQL query executed against your database becomes

SELECT * FROM Recipes WHERE Name = ''; DROP TABLE Recipes; --'

Simply by entering text into the search form of your app, the attacker has deleted the entire Recipes table from your app! That’s catastrophic, but an SQL injection vulnerability provides more or less unfettered access to your database.Even if you’ve set up database permissions correctly to prevent this sort of destructive action, attackers will likely be able to read all the data from your database, including your users’ details.

The simple way to prevent this from happening is to avoid creating SQL queries by hand this way. If you do need to write your own SQL queries, don’t use string concatenation, as in listing 29.6. Instead, use parameterized queries, in which the (potentially unsafe) input data is separate from the query itself, as shown here.

Listing 29.7 Avoiding SQL injection by using parameterization

public IList<User> FindRecipe(string search)
{
return _context.Recipes
.FromSqlRaw( "SELECT * FROM Recipes WHERE Name = '{0}'", ❶
search) ❷
.ToList();
}

❶ The SQL query uses a placeholder {0} for the parameter.
❷ The dangerous input is passed as a parameter, separate from the query.

Parameterized queries are not vulnerable to SQL injection attacks, so the attack presented earlier won’t work. If you use EF Core or other ORMs to access data using standard LINQ queries, you won’t be vulnerable to injection attacks. EF Core automatically creates all SQL queries using parameterized queries to protect you. Even if you’re using the low-level ADO.NET database APIs, stick to parameterized queries!

NOTE I’ve talked about SQL injection attacks only in terms of a relational database, but this vulnerability can appear in NoSQL and document databases too. Always use parameterized queries or the equivalent, and don’t craft queries by concatenating strings with user input.

Injection attacks have been the number-one vulnerability on the web for more than a decade, so it’s crucial to be aware of them and how they arise. Whenever you need to write raw SQL queries, make sure that you always use parameterized queries.

The next vulnerability is also related to attackers accessing data they shouldn’t be able to. It’s a little subtler than a direct injection attack but is trivial to perform; the only skill the attacker needs is the ability to count.

29.4.3 Preventing insecure direct object references‌

Insecure direct object reference is a bit of a mouthful, but it means users accessing things they shouldn’t by noticing patterns in URLs. Let’s revisit our old friend the recipe app. As a reminder, the app shows you a list of recipes. You can view any of them, but you can edit only recipes you created yourself. When you view someone else’s recipe, there’s no Edit button visible.‌

A user clicks the Edit button on one of their recipes and notices that the URL is /Recipes/Edit/120. That 120 is a dead giveaway as being the underlying database ID of the entity you’re editing. A simple attack would be to change that ID to gain access to a different entity, one that you wouldn’t normally have access to. The user could try entering /Recipes/Edit/121. If that lets them edit or view a recipe that they shouldn’t be able to, you have an insecure direct object reference vulnerability.

The solution to this problem is simple: you should have resource-based authorization in your endpoint handlers. If a user attempts to access an entity they’re not allowed to access, they should get a permission-denied error. They shouldn’t be able to bypass your authorization by typing a URL directly into the search bar of their browser.

In ASP.NET Core apps, this vulnerability typically arises when you attempt to restrict users by hiding elements from your UI, such as by hiding the Edit button. Instead, you should use resource-based authorization, as discussed in chapter 24.

WARNING You must always use resource-based authorization to restrict which entities a user can access. Hiding or disabling UI elements provides an improved user experience, but it isn’t a security measure.

You can sidestep this vulnerability somewhat by avoiding integer IDs for your entities in the URLs, perhaps by using a pseudorandom globally unique identifier (GUID) such as C2E296BA-7EA8-4195-9CA7-C323304CCD12 instead.

This makes the process of guessing other entities harder, as you can’t simply add 1 to an existing number, but it’s masking the problem rather than fixing it. Nevertheless, using GUIDs can be useful when you want to have publicly accessible pages that don’t require authentication but don’t want their IDs to be easily discoverable.

The final section in this chapter doesn’t deal with a single vulnerability. Instead, I discuss a separate but related problem: protecting your users’ data.

29.4.4 Protecting your users’ passwords and data‌

For many apps, the most sensitive data you’ll be storing is the personal data of your users. This could include emails, passwords, address details, or payment information. You should be careful when storing any of this data. As well as presenting an inviting target for attackers, you may have legal obligations for how you handle it, such as data protection laws and Payment Card Industry (PCI) compliance requirements.

The easiest way to protect yourself is to not store data you don’t need. If you don’t need your user’s address, don’t ask for it. That way, you can’t lose it! Similarly, if you use a third- party identity service to store user details, as described in chapter 23, you won’t have to work as hard to protect your users’ personal information.

If you store user details in your own app or build your own identity provider, then you need to make sure to follow best practices when handling user information. The new project templates that use ASP.NET Core Identity follow most of these practices by default, so I highly recommend you start from one of these. You need to consider many aspects, too many to go into detail here,1 but they include the following:

• Never store user passwords anywhere directly. You should store only cryptographic hashes computed using an expensive hashing algorithm, such as BCrypt or PBKDF2.

• Don’t store more data than you need. You should never store credit card details.

• Allow users to use multifactor authentication (MFA) to sign in to your site.

Prevent users from using passwords that are known to be weak or compromised, such as disallowing dictionary words, sequential characters, and so on.

• Mark authentication cookies as http (so that they can’t be read using JavaScript) and secure so they’ll be sent only over an HTTPS connection, never over HTTP. Where possible, you should also mark your cookies as SameSite=strict. See the documentation for details: http://mng.bz/a11m.

• Don’t expose whether a user is already registered with your app. Leaking this information can expose you to enumeration attacks.

TIP You can learn more about website enumeration in this video tutorial by Troy Hunt: http://mng.bz/PAAA.

These guidelines represent the minimum you should be doing to protect your users. The most important thing is to be aware of potential security problems as you’re building your app. Trying to bolt on security at the end is always harder than thinking about it from the start, so it’s best to think about it earlier rather than later.

This chapter has been a whistle-stop tour of things to look out for. We’ve touched on most of the big names in security vulnerabilities, but I strongly encourage you to check out the other resources mentioned in this chapter. They provide a more exhaustive list of things to consider, complementing the defenses mentioned in this chapter. On top of that, don’t forget about input validation and mass assignment/overposting, as discussed in chapter 16. ASP.NET Core includes basic protections against some of the most common attacks, but you can still shoot yourself in the foot. Make sure it’s not your app making headlines for being breached!

29.5 Summary

XSS attacks involve malicious users injecting content into your app, typically to run malicious JavaScript when users browse your app. You can prevent XSS injection attacks by always encoding unsafe input before writing it to a page. Razor Pages do this automatically unless you use the @Html.Raw() method, so use it sparingly and carefully.

CSRF attacks are a problem for apps that use cookie-based authentication, such as ASP.NET Core Identity. These attacks rely on the fact that browsers automatically send cookies to a website. A malicious website could create a form that POSTs to your site, and the browser will send the authentication cookie with the request. This allows malicious websites to send requests as though they’re the logged-in user.

You can mitigate CSRF attacks using antiforgery tokens, which involve writing a hidden field in every form that contains a random string based on the current user. A similar token is stored in a cookie. A legitimate request will have both parts, but a forged request from a malicious website will have only the cookie half; it cannot re-create the hidden field in the form. By validating these tokens, your API can reject forged requests.

The Razor Pages framework automatically adds antiforgery tokens to any forms you create using Razor and validates the tokens for inbound requests. You can disable the validation check if necessary, using the [IgnoreAntiForgeryToken] attribute.

Browsers won’t allow websites to make JavaScript AJAX requests from one app to others at different origins. To match the origin, the app must have the same scheme, domain, and port. If you wish to make cross-origin requests like this, you must enable CORS in your API.

CORS uses HTTP headers to communicate with browsers and defines which origins can call your API. In ASP.NET Core, you can define multiple policies, which can be applied globally to your whole app or to specific controllers and actions.

You can add the CORS middleware by calling UseCors() on WebApplication and optionally providing the name of the default CORS policy to apply. You can also apply CORS to endpoints by calling RequireCors() or adding the [EnableCors] attribute and providing the name of the policy to apply.

Configure the policies for your application by calling AddCors() on WebApplicationBuilder and adding policies in the lambda using AddPolicy(). A policy defines which origins are allowed to call an endpoint, which HTTP methods they can use, and which headers are allowed.

Open redirect attacks use the common returnURL mechanism after logging in to redirect users to malicious websites. You can prevent this attack by ensuring that you redirect only to local URLs—URLs that belong to your app.

Insecure direct object references are a common problem where you expose the ID of database entities in the URL. You should always verify that users have permission to access or change the requested resource by using resource-based authorization in your action methods.

SQL injection attacks are a common attack vector when you build SQL requests manually. Always use parameterized queries when building requests or use a framework like EF Core, which isn’t vulnerable to SQL injection.

The most sensitive data in your app is often the data of your users. Mitigate this risk by storing only data that you need. Ensure that you store passwords only as a hash, protect against weak or compromised passwords, and provide the option for MFA. ASP.NET Core Identity provides all of this out of the box, so it’s a great choice if you need to create an identity provider.

  1. In 2020 the National Institute of Standards and Technology (NIST) updated its Digital Identity Guidelines on handling user details, which contains some great advice. See http://mng.bz/6gRA.

Leave a Reply

Your email address will not be published. Required fields are marked *