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:
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.
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.
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.
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.
dotnet run
a) Email: [email protected]
b) Password: rUj5jtV8jrTyHnx!
a) Email: [email protected]
b) Password: 6GKqqtQQTii92ke!
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.
Let's take a look at the steps for this recipe:
code .
[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.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using System.Threading.Tasks;
using OnlineBankingApp.Models;
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.
UserManager<Customer> _userManager;
public FundTransferIsOwnerAuthorizationHandler (UserManager<Customer> userManager){
_userManager = userManager;
}
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;
}
}
public class FundTransferOwnerRequirement : IAuthorizationRequirement { }
}
FundTransferOwnerRequirement doesn't need to have any properties or data, so we will leave the class empty.
using OnlineBankingApp.Authorization;
using Microsoft.AspNetCore.Authorization
.Infrastructure;
services.AddAuthorization(options => {
options.AddPolicy("Owner", policy =>
policy.Requirements.Add(new FundTransferOwnerRequirement()));
});
services.AddScoped<IAuthorizationHandler, FundTransferIsOwnerAuthorizationHandler>();
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
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;
}
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:
Notice that Axl's account no longer has access to Stanley's fund transfer details page and was redirected to the Access denied page.
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.
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.
dotnet run
a) Email: [email protected]
b) Password: 6GKqqtQQTii92ke!
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.
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.
Let's take a look at the steps for this recipe:
code .
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.
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
dotnet run
a). Email: [email protected]
b). Password: 6GKqqtQQTii92ke!
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.
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.
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.
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.
Let's take a look at the steps for this recipe.
code .
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.
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.
using OnlineBankingApp.Authorization;
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.
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.
dotnet run
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:
Axl is pre-assigned a PendingCustomer role (see ModelsSeedData.cs), which prevents him from submitting a fund transfer based on the policy we created.
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));
}
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.
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.
Here are the steps:
dotnet run
a) Email: [email protected]
b) Password: rUj5jtV8jrTyHnx!
The preceding test shows that this page is vulnerable to an open redirect attack.
Let's take a look at the steps for this recipe.
code .
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.
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.
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.
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.
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.
18.117.196.184