Chapter 6: Broken Access Control

Authorization is just as significant and essential as authentication. It defines what an authenticated user can perform and execute, and resources and web pages need to have defined privileges to limit unauthorized access. Permission bypass and missing or improper access controls are some of the broken access control vulnerabilities discovered in an ASP.NET Core web application.

In this chapter, we're going to cover the following recipes:

  • Fixing insecure direct object references (IDOR)
  • Fixing improper authorization
  • Fixing missing access control
  • Fixing open redirect vulnerabilities

By the end of this chapter, you will have learned how to use the built-in authorization mechanism in ASP.NET Core. You will properly implement role-based authorization to prevent unauthorized access to resources in your web application. Also, you will see how to utilize safer redirection methods to prevent open redirection attacks.

Technical requirements

This book was written and designed to use with Visual Studio Code, Git, and .NET 5.0. Code examples in recipes are presented in ASP.NET Core Razor pages. The sample solution also uses SQLite as the database (DB) engine for a more simplified setup. The complete code examples for this chapter are available at https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook/tree/main/Chapter06.

Fixing IDOR

When accessing a record in a DB, we often use a form of identifier (ID) that uniquely identifies a dataset. The DB design and structure rely on these keys, and sometimes they can be easily guessed or enumerated. Adversaries can find these identifiers in your requests to your ASP.NET Core web pages. If not adequately safeguarded with access controls, a malicious user can view, modify, or— at worst—delete these records.

In this recipe, we will discover the IDOR vulnerability in our code and mitigate the problem by using the identity of the authenticated customer.

Getting ready

For the recipes of this chapter, we will need the sample Online Banking app.

Open the command shell and download the sample Online Banking app by cloning the ASP.NET-Core-Secure-Coding-Cookbook repository, as follows:

git clone https://github.com/PacktPublishing/ASP.NET-Core-Secure-Coding-Cookbook.git

Run the sample app to verify that there are no build or compile errors. In your command shell, navigate to the sample app folder at Chapter06insecure-direct-object-referenceseforeOnlineBankingApp and run the following command:

dotnet build

The dotnet build command will build our sample OnlineBankingApp project and its dependencies.

Let's see in action how IDOR vulnerabilities can be exploited.

Testing IDOR

Here are the steps:

  1. Navigate to Terminal | New Terminal in the menu or do this by simply pressing Ctrl + Shift + ' in VS Code.
  2. Type the following command in the terminal to build and run the sample app:

    dotnet run

  3. Open a browser and go to https://localhost:5001/Fundtransfers/Details?id=1.
  4. Log in using the following credentials:

    a) Email: [email protected]

    b) Password: rUj5jtV8jrTyHnx!

  5. Once authenticated, you will be redirected to Stanley's fund transfer details page, as shown in the following screenshot:

    Figure 6.1 – Fund transfer details page

    Figure 6.1 – Fund transfer details page

  6. Click on Logout to log out from the sample solution, as shown in the following screenshot:

    Figure 6.2 – Logout link

    Figure 6.2 – Logout link

  7. Go to https://localhost:5001/FundTransfers/Details?id=1.
  8. Now, log in using Axl's credentials:

    a) Email: [email protected]

    b) Password: 6GKqqtQQTii92ke!

  9. Notice in the following screenshot that Axl is able to see Stanley's fund transfer details page:
Figure 6.3 – Unauthorized access

Figure 6.3 – Unauthorized access

The preceding test shows that this page is susceptible to an IDOR security bug.

In this recipe, we will fix the IDOR vulnerability in code by adding a validation check to ascertain whether a specific user can see the fund transfer details page.

How to do it…

Let's take a look at the steps for this recipe:

  1. From the starting exercise folder, launch VS Code by typing the following command:

    code .

  2. Open ModelsFundTransfer.cs and change the ID property from int to a Guid type. Globally unique identifiers (GUIDs) are unique identifiers and are harder to guess:

        [Key]

        public Guid ID { get; set; }

    Annotating the ID property with the Key attribute makes this property the primary key for Entity Framework to identify.

  3. Under the Services folder, create a new file and name it FundTransferIsOwnerAuthorizationHandler.cs.
  4. In FundTransferIsOwnerAuthorizationHandler.cs, add references to the following namespaces:

    using Microsoft.AspNetCore.Authorization;

    using Microsoft.AspNetCore.Identity;

    using System.Threading.Tasks;

    using OnlineBankingApp.Models;

  5. Next, define a FundTransferIsOwnerAuthorizationHandler class that inherits from AuthorizationHandler:

    namespace OnlineBankingApp.Authorization {

        public class         FundTransferIsOwnerAuthorizationHandler             : AuthorizationHandler<                FundTransferOwnerRequirement,                 FundTransfer>{

        }

    }

    An authorization handler—as the name implies—handles authorization, and in our preceding highlighted code, it determines whether a user will have access or not.

  6. Using dependency injection (DI), use the UserManager service to be able to retrieve the user ID information from the currently logged-in customer:

            UserManager<Customer> _userManager;

            public FundTransferIsOwnerAuthorizationHandler             (UserManager<Customer>             userManager){

                _userManager = userManager;

            }

  7. Inside the FundTransferIsOwnerAuthorizationHandler class, define a Task object that will handle the authorization check using the passed requirement and resource arguments:

        protected override Task

        HandleRequirementAsync(AuthorizationHandlerContext         context,

            FundTransferOwnerRequirement requirement,

            FundTransfer resource){

                if (context.User == null || resource == null){

                    return Task.CompletedTask;

                }

                if (resource.CustomerID ==                    _userManager.GetUserId                         (context.User)){

                    context.Succeed(requirement);

                }

                return Task.CompletedTask;

            }

        }

  8. Define a FundTransferOwnerRequirement class that will inherit from the IauthorizationRequirement empty marker interface within the same FundTransferIsOwnerAuthorizationHandler.cs file:

    public class FundTransferOwnerRequirement :    IAuthorizationRequirement { }

    }

    FundTransferOwnerRequirement doesn't need to have any properties or data, so we will leave the class empty.

  9. Open Startup.cs and include the following namespace references:

    using OnlineBankingApp.Authorization;

    using Microsoft.AspNetCore.Authorization

    .Infrastructure;

  10. In ConfigureServices, add a new authorization policy and register the authorization handler we created in Step 3:

    services.AddAuthorization(options => {

        options.AddPolicy("Owner", policy =>

            policy.Requirements.Add(new             FundTransferOwnerRequirement()));

    });

    services.AddScoped<IAuthorizationHandler,    FundTransferIsOwnerAuthorizationHandler>();

  11. Next, open the PagesFundTransfersDetails.cshtml.cs file and add the following namespace references:

    using Microsoft.AspNetCore.Authorization;

    using Microsoft.AspNetCore.Identity;

  12. Through DI, add the following highlighted code from the authorization service we registered in Step 5 into the DetailsModel constructor:

    protected IAuthorizationService _authorizationService     { get; }

    protected UserManager<Customer> _userManager { get; }

    public DetailsModel(OnlineBankingApp.Data     .OnlineBankingAppContext context,

               IAuthorizationService authorizationService,

               UserManager<Customer> userManager)

    {

        _context = context;

        _userManager = userManager;

        _authorizationService = authorizationService;

    }

  13. Refactor the whole code under the OnGetAsync page handler:

    public async Task<IActionResult> OnGetAsync(Guid? id)

    {

        if (!id.HasValue){

            return NotFound();

        }

        if (!User.Identity.IsAuthenticated){

            return Challenge();

        }

        fundTransfer = await _context.FundTransfer

                    .Where(f => f.ID == id)

                    .Include(f => f.Customer)

                    .OrderBy(f => f.TransactionDate)

                    .FirstOrDefaultAsync<FundTransfer>();

        var isAuthorized = await         _authorizationService.AuthorizeAsync                  (User, fundTransfer,"Owner");

        if (!isAuthorized.Succeeded){

            return Forbid();

        }

        return Page();

    }

    With the steps we performed, we have implemented a more robust way of authorization using a policy-based authorization approach.

    Repeat the steps in the Testing IDOR section to verify that the fix worked, but instead of using the IDOR-vulnerable Uniform Resource Locator (URL), go to https://localhost:5001/FundTransfers/Details?id=7c281d46-f2ab-4027-a4d4-3bb97a60012c, and you should see the following message on your screen:

Figure 6.4 – Access denied message

Figure 6.4 – Access denied message

Notice that Axl's account no longer has access to Stanley's fund transfer details page and was redirected to the Access denied page.

How it works…

First, we change our FundTransfer primary key into a type that cannot be guessed easily. We use the Guid type to allow us to have a unique ID (UID) as our Key for each fund transfer:

    [Key]

    public Guid ID { get; set; }

We then implement policy-based authorization by first creating an authorization handler. Inside the FundTransferIsOwnerAuthorizationHandler class is the code that determines if the resource's (fund transfer's) CustomerID matches that of the customer's user ID. If the requirement is satisfied, a call to the Succeed method of AuthorizationHandlerContext indicates a successful evaluation:

if (resource.CustomerID ==    _userManager.GetUserId(context.User)){

    context.Succeed(requirement);

}

The authorization handler is registered as a service, and a preconfigured policy is added using the AddScoped and the AddPolicy methods respectively:

services.AddAuthorization(options => {

    options.AddPolicy("Owner", policy =>

        policy.Requirements.Add(new             FundTransferOwnerRequirement()));

});

services.AddScoped<IAuthorizationHandler,    FundTransferIsOwnerAuthorizationHandler>();

We utilize these services via DI in our DetailsModel page model.

Fixing improper authorization

Incorrectly using ASP.NET Core's authorization components could lead to insecure code. The authorization feature offers a simple and declarative way to impose authorization, but mistakes can occur in implementing this. In this recipe, we will correctly implement the role-based authorization feature of ASP.NET Core in our sample Online Banking application.

Run the sample app to verify that there are no build or compile errors. In your command shell, navigate to the sample app folder at Chapter06improper-authorizationeforeOnlineBankingApp.

Let's see in action how improper authorization can lead someone to use functions a customer is not authorized to use.

Testing improper authorization

Here are the steps:

  1. Navigate to Terminal | New Terminal in the menu or do this by simply pressing Ctrl + Shift + ' in VS Code.
  2. Type the following command in the terminal to build and run the sample app:

    dotnet run

  3. Open a browser and go to https://localhost:5001/FundTransfers/Create.
  4. Log in using the following credentials:

    a) Email: [email protected]

    b) Password: 6GKqqtQQTii92ke!

  5. Once authenticated, you will be redirected to a page where you can make a fund transfer.

Our sample Online Banking solution had only created Axl's customer account; thus, his roles are Customer and PendingCustomer. Until Axl's account moves into an ActiveCustomer role, he shouldn't be able to make a fund transfer.

Getting ready

We will use the Online Banking app we used in the previous recipe. Using VS Code, open the sample Online Banking app folder at Chapter06missing-access-controleforeOnlineBankingApp.

You can also perform the steps in this folder for Fixing improper authorization recipe.

How to do it…

Let's take a look at the steps for this recipe:

  1. From the starting exercise folder, launch VS Code by typing the following command:

    code .

  2. Open the PagesFundTransfersCreate.cshtml.cs file and notice the Authorize annotation on top of the CreateModel class:

    namespace OnlineBankingApp.Pages.FundTransfers

    {

        [Authorize(Roles = "Customer,ActiveCustomer")]

        public class CreateModel : AccountPageModel

        {

            private readonly OnlineBankingApp.Data             .OnlineBankingAppContext _context;

            public CreateModel (OnlineBankingApp.Data             .OnlineBankingAppContext context)

            {

                _context = context;

            }

    // code removed for brevity

    The Authorize annotation appears to have been used properly, but not quite. The CreateModel page model would only be open to customers who have a Customer OR an ActiveCustomer role. Setting the Authorize annotation in this format means customers with either role can send money, which is not what we expect based on our business rule, allowing only active customers to make fund transfers.

  3. Change the way the Authorize annotation is formatted using the following code:

    namespace OnlineBankingApp.Pages.FundTransfers

    {

        [Authorize(Roles = "Customer")]

        [Authorize(Roles = "ActiveCustomer")]

        public class CreateModel : AccountPageModel

        {

            private readonly OnlineBankingApp.Data             .OnlineBankingAppContext _context;

            Public CreateModel(OnlineBankingApp.Data             .OnlineBankingAppContext context)

            {

                _context = context;

            }

    // code removed for brevity

  4. Navigate to Terminal | New Terminal in the menu or do this by simply pressing Ctrl + Shift + ' in VS Code.
  5. Type the following command in the terminal to build and run the sample app:

    dotnet run

  6. Open a browser and go to https://localhost:5001/Fundtransfers/Create.
  7. Log in with the following credentials:

    a). Email: [email protected]

    b). Password: 6GKqqtQQTii92ke!

  8. Notice that you will be redirected to the Access denied page, as shown in the following screenshot:
Figure 6.5 – Access denied page

Figure 6.5 – Access denied page

Setting the AuthorizeAttribute property configures the necessary authorization in the CreateModel page model. This requires that an authenticated user has both Customer AND ActiveCustomer roles.

How it works…

Declarative role checks enable web developers to add authorization in a page model easily, but there is a big difference between the annotations. For example, have a look at this one:

    [Authorize(Roles = "Customer,ActiveCustomer")]

Now, contrast it with these annotations:

    [Authorize(Roles = "Customer")]

    [Authorize(Roles = "ActiveCustomer")]

The first one indicates that an authenticated user with either a Customer or an ActiveCustomer role can access the fund transfer page. The latter specifies that a customer needs both roles to have the authority to send money.

Tip

A policy-based authorization check is also a necessary technique to accompany declarative authorization, to ensure a user is authorized to view a fund transfer. Please refer to the Fixing IDOR recipe for more information and details on how to implement this type of authorization.

Fixing missing access control

An access control vulnerability can allow a malicious actor to access your ASP.NET Core web application just by simply registering an account and getting authenticated. This security flaw can lead to unauthorized access to sensitive information.

In this recipe, we add roles to the sample Online Banking app to integrate a policy-based authorization.

Getting ready

We will use the Online Banking app we used in the previous recipe. Using VS Code, open the sample Online Banking app folder at Chapter06missing-access-controleforeOnlineBankingApp.

You can also perform the steps in this folder for the Fixing missing access control recipe.

How to do it…

Let's take a look at the steps for this recipe.

  1. From the starting exercise folder, launch VS Code by typing the following command:

    code .

  2. Open the PagesFundTransfersCreate.cshtml.cs file and notice the Authorize annotation on top of the CreateModel class:

    namespace OnlineBankingApp.Pages.FundTransfers

    {

        [Authorize]

        public class CreateModel : AccountPageModel

        {

            private readonly OnlineBankingApp.Data             .OnlineBankingAppContext _context;

            public CreateModel(OnlineBankingApp.Data             .OnlineBankingAppContext context)

            {

                _context = context;

            }

    // code removed for brevity

    The Authorize attribute in the CreateModel class provides the most basic authorization indicating that this Razor pages model requires authorization. However, a lack of defined roles as to which types of customers can make a fund transfer opens up an opportunity for an adversary to abuse this.

  3. We need to implement policy-based authorization with criteria defined based on the current roles that our customer has. Under the Models folder, create a new file, name it PrincipalPermission.cs, and add the following code:

    using System;

    using System.Collections.Generic;

    using Microsoft.AspNetCore.Authorization;

    using OnlineBankingApp.Models;

    namespace OnlineBankingApp.Authorization{

        public static class PrincipalPermission{

            public static List             <Func<AuthorizationHandlerContext, bool>>                Criteria = new List<Func                 <AuthorizationHandlerContext, bool>>

        {

                CanCreateFundTransfer

        };

        

            public static bool CanCreateFundTransfer              (this AuthorizationHandlerContext ctx){

                return ctx.User.IsInRole                 (Role.ActiveCustomer.ToString());

            }

        }

    }

    In the preceding code snippet, we used Func to fulfill a policy. Func is a delegate that will point to our CanCreateFundTransfer method. We also created an instance of List<Func<AuthorizationHandlerContext, bool>> to configure a Criteria list for our policy. We defined the CanCreateFundTransfer method as one of our criteria, indicating that only customers with an ActiveCustomer role can create fund transfers.

    Note

    You can define more criteria for a customer to be able to submit fund transfers, but to simplify the example, we will use the customer's current role.

  4. Open Startup.cs, and in ConfigureServices, add a reference to OnlineBankingApp.Authorization, which is the namespace for our PrincipalPermission class:

    using OnlineBankingApp.Authorization;

  5. Include the following highlighted code in the authorization middleware:

    services.AddAuthorization(options =>

    {

        options.FallbackPolicy = new         AuthorizationPolicyBuilder()

                .RequireAuthenticatedUser()

                .Build();

        foreach (var criterion in PrincipalPermission         .Criteria)

        {

            options.AddPolicy(criterion.Method.Name,

                    policy =>

                      policy.RequireAssertion(criterion));

        }

    });

    We loop into each of the criteria lists we defined and create an authorization policy for each.

  6. Open PagesFundTransfersCreate.cshtml.cs and annotate the CreateModel page model with the highlighted code:

    namespace OnlineBankingApp.Pages.FundTransfers

    {

        [Authorize(Policy =        nameof(PrincipalPermission             .CanCreateFundTransfer))]

        public class CreateModel : AccountPageModel

        {

    // code removed for brevity

    Placing the preceding highlighted attribute will apply the authorization policy that we added to the authorization service.

  7. Navigate to Terminal | New Terminal in the menu or do this by simply pressing Ctrl + Shift + ' in VS Code.
  8. Type the following command in the terminal to build and run the sample app:

    dotnet run

  9. Open a browser and go to https://localhost:5001/Fundtransfers/Create.
  10. Log in with the following credentials:

    a) Email: [email protected]

    b) Password: 6GKqqtQQTii92ke!

    Notice that the user is redirected to the https://localhost:5001/Identity/Account/AccessDenied?ReturnUrl=%2FFundTransfers%2FCreate Access denied URL:

Figure 6.6 – Access denied page for users with PendingCustomer roles

Figure 6.6 – Access denied page for users with PendingCustomer roles

Axl is pre-assigned a PendingCustomer role (see ModelsSeedData.cs), which prevents him from submitting a fund transfer based on the policy we created.

How it works…

The policy-based approach gives ASP.NET Core web developers the granularity needed to define authorization matrices. In our preceding recipe, we used a simple example of using roles as criteria for our authorization policy. In fulfilling a policy, we supplied a List of Func<AuthorizationHandlerContext, bool> that holds each of the Criteria we defined:

public static List<Func<AuthorizationHandlerContext, bool>>        Criteria = new List<Func             <AuthorizationHandlerContext, bool>>

{

    CanCreateFundTransfer,

    CanViewFundTransfer

};

The Criteria represent a delegate that will be used to set the conditional access. In our case, we will use the customer's role as a criterion, but you can expand it if necessary:

public static bool CanCreateFundTransfer(this         AuthorizationHandlerContext ctx)

{

    return ctx.User.IsInRole(Role.ActiveCustomer         .ToString());

}

Finally, we use the RequireAssertion policy to build our policies with our List of Criteria:

foreach (var criterion in PrincipalPermission.Criteria)

{

    options.AddPolicy(criterion.Method.Name,

        policy => policy.RequireAssertion(criterion));

}   

Fixing open redirect vulnerabilities

A user can be tricked into clicking a link generated from your ASP.NET Core web application, but this can eventually redirect them to a malicious website. Open redirection can happen when a user-controlled parameter determines that the URL to redirect to has no validation or whitelisting. In this recipe, we will remediate the risk of open redirect attacks in code by utilizing safer redirect methods.

First, let's take a look at how an open redirect vulnerability is exploited.

Getting ready

We will use the Online Banking app we used in the previous recipe. Using VS Code, open the sample Online Banking app folder at Chapter06unvalidated-redirecteforeOnlineBankingApp.

You can also perform the steps in this folder for the Fixing open redirect vulnerability recipe.

Testing open redirection

Here are the steps:

  1. Navigate to Terminal | New Terminal in the menu or do this by simply pressing Ctrl + Shift + ' in VS Code.
  2. Type the following command in the terminal to build and run the sample app:

    dotnet run

  3. Open a browser and go to https://localhost:5001/Identity/Account/Login?ReturnUrl=https://www.packtpub.com.
  4. Log in using the following credentials:

    a) Email: [email protected]

    b) Password: rUj5jtV8jrTyHnx!

  5. Once authenticated, you will be redirected to the Packt Publishing website.

    The preceding test shows that this page is vulnerable to an open redirect attack.

How to do it…

Let's take a look at the steps for this recipe.

  1. From the starting exercise folder, launch VS Code by typing the following command:

    code .

  2. Open AreasIdentityPagesAccountLogin.cshtml.cs and notice the Redirect method call:

    public async Task<IActionResult> OnPostAsync(string   url = null)

    {

      . . . .

    // code removed for brevity

        var signInResult = await _signInManager         .PasswordSignInAsync(Input.Email,             Input.Password, Input.RememberMe,                lockoutOnFailure: false);

        if (signInResult .Succeeded)

        {

            _log.LogInformation("User logged in.");

            if (string.IsNullOrEmpty(HttpContext             .Session.GetString(SessionKey)))

            {

                HttpContext.Session.SetString(SessionKey,                Input.Email);

            }

            return Redirect(url);

        }

    // code removed for brevity

    The Redirect method, when invoked, sends a temporary redirect response to the browser. With no URL validation in place, the URL redirection can be abused and sent to a website controlled by an attacker whenever a tricked customer clicks a malicious URL.

  3. Another security flaw found in this sample Online Banking app is in its logout page redirection. Open AreasIdentityPagesAccountLogout.cshtml.cs and go to the OnGet method of the page method:

    public async Task<IActionResult> OnGet(string url =  null)

    {

        await _signInManager.SignOutAsync();

        _log.LogInformation("User logged out.");

        if (url != null)

        {

            return Redirect(url);

        }

        else

        {

            return RedirectToPage();

        }

    }

    // code removed for brevity

    Again, the redirection is invalidated, and an adversary can craft a URL that could redirect to a hacker-controlled website delivered through phishing or some other deceptive means.

  4. To remediate these security flaws, open AreasIdentityPagesAccountLogin.cshtml.cs and change the Redirect method to LocalRedirect:

    if (ModelState.IsValid)

    {

        // This doesn't count login failures towards     account lockout

        // To enable password failures to trigger account     lockout, set lockoutOnFailure: true

        var signInResult = await _signInManager         .PasswordSignInAsync(Input.Email,            Input.Password, Input.RememberMe,                 lockoutOnFailure: false);

        if (signInResult.Succeeded)

        {

            _log.LogInformation("User logged in.");

            if (string.IsNullOrEmpty           (HttpContext.Session.GetString(SessionKey)))

            {

                HttpContext.Session.SetString(SessionKey,                Input.Email);

            }

            return LocalRedirect(url);

        }

    // code removed for brevity

    The LocalRedirect method performs the same redirection, except it will throw an InvalidOperationException exception when the URL is trying to redirect to a website that is not local.

  5. Another way to fix this security bug is to use the Url.IsLocalUrl method. Open AreasIdentityPagesAccountLogout.cshtml.cs and go to the OnGet method of the page method. Change the method from OnGet to OnPost for this method to be invoked on HyperText Transfer Protocol (HTTP) POST requests, not HTTP GET requests. Replace the Redirect method call with 
a validation check using the IsLocalUrl method:

    public async Task<IActionResult> OnPost(string url =  null)

    {

        await _signInManager.SignOutAsync();

        _log.LogInformation("User logged out.");

        if (url != null)

        {

            if (Url.IsLocalUrl(url))

                return Redirect(url);

            else

                return RedirectToPage();

        }

        else

        {

            return RedirectToPage();

        }

    }

    // code removed for brevity

    A call to the Url.IsLocalUrl method checks if the URL is local, preventing the customer running the risk of getting redirected to an arbitrary URL.

How it works…

A malicious website could imitate a legitimate website and deceive the user into using their credentials to log in, ultimately stealing usernames and passwords from victims. We use both the LocalRedirect and Url.IsLocalUrl methods, replacing the dangerous Redirect method to validate the URLs received as parameters. Implementing these safer functions can protect us from getting redirected to unwanted URLs.

If certain use cases require users to be redirected to an external URL, a whitelist validation technique must be applied to determine whether a URL is allowed.

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

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