Attacks on ASP.NET Core web applications can happen at any given moment in time. Developers must empower their security teams to reconstruct an incident by generating adequate logs from web applications. Logging the right information will help determine an event's details and identify critical data for auditing purposes. The downside of failing to log key security information prevents security teams from producing proper analysis or reports. Too much logging, however, can lead to sensitive data exposure. Applying a necessary and immediate response to act on such security events is only possible through active monitoring. Developers must enable monitoring in the logs that our ASP.NET Core web applications generate for a more real-time defense.
In this chapter, we're going to cover the following recipes:
By the end of this chapter, you will have learned how to correctly add proper exception logging in our sample Online Banking app, how to log a critical DB transaction, how to prevent logging too much data or information, and how to enable security monitoring.
This book was written and designed to use with Visual Studio Code (VS Code), Git, and .NET Core 5.0. The code examples in the recipes are presented mostly in ASP.NET Core Razor Pages. The sample solution also uses SQLite as the 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/Chapter11.
Security-related events such as user authentication or enabling and disabling of two-factor authentication (2FA)—when this occurs—must be recorded and kept track of. These events are essential for auditing in order to understand the sequence of events when a security incident happens.
In this recipe, we will fix the insufficient logging of security-related exceptions by utilizing ASP.NET Core's built-in logging provider.
For the recipes of this chapter, we will need a 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 Chapter11insufficient-logging-exceptioneforeOnlineBankingApp and run the following command:
dotnet build
The dotnet build command will build our sample OnlineBankingApp project and its dependencies.
Let's take a look at the steps for this recipe:
code .
public async Task<IActionResult> OnGet()
{
var customer = await _customerManager.GetUserAsync(User);
if (customer== null)
{
return NotFound($"Unable to load customer with ID '{_ customerManager.GetUserId(User)}'.");
}
if (!await _customerManager .GetTwoFactorEnabledAsync(customer))
{
throw new InvalidOperationException($"Cannot disable 2FA for customer with ID '{_customerManager.GetUserId(User)}' as it's not currently enabled.");
}
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
var customer = await _customerManager.GetUserAsync(User);
if (customer == null)
{
return NotFound($"Unable to load customer with ID '{_customerManager.GetUserId(User)}'.");
}
var disable2faResult = await _customerManager.SetTwoFactorEnabledAsync (customer, false);
if (!disable2faResult.Succeeded)
{
throw new InvalidOperationException ($"Unexpected error occurred disabling 2FA for customer with ID '{_customerManager .GetUserId (User)}'.");
}
_logger.LogInformation("Customer with ID '{UserId}' has disabled 2fa.", _customerManager.GetUserId(User));
StatusMessage = "2fa has been disabled. You can reenable 2fa when you setup an authenticator app";
return RedirectToPage("./TwoFactorAuthentication");
}
Both methods have a line of code where an InvalidOperationException exception is thrown. The exception indicates that an attempt to disable 2FA was made but failed. These events should be considered anomalies and logged. These events can be considered anomalies, and each should be logged.
public async Task<IActionResult> OnGet()
{
var customer = await _customerManager.GetUserAsync(User);
if (customer == null)
{
return NotFound($"Unable to load customer with ID '{_customerManager.GetUserId(User)}'.");
}
if (!await _customerManager.GetTwoFactorEnabledAsync (user))
{
_logger.LogError($"Cannot disable 2FA for customer with ID '{_customerManager .GetUserId(User)}' as it's not currently enabled.");
throw new InvalidOperationException($"Cannot disable 2FA for customer with ID '{_customerManager.GetUserId(User)}' as it's not currently enabled.");
}
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
var customer = await _customerManager.GetUserAsync(User);
if (customer == null)
{
return NotFound($"Unable to load customer with ID '{_customerManager.GetUserId(User)}'.");
}
var disable2faResult = await _customerManager.SetTwoFactorEnabledAsync (customer, false);
if (!disable2faResult.Succeeded)
{
_logger.LogError($"Unexpected error occurred disabling 2FA for customer with ID '{_customerManager.GetUserId (User)}'.");
throw new InvalidOperationException ($"Unexpected error occurred disabling 2FA for customer with ID'{_customerManager .GetUserId(User)}'.");
}
_logger.LogInformation("Customer with ID '{UserId}' has disabled 2fa.", _customerManager.GetUserId(User));
StatusMessage = "2fa has been disabled. You can reenable 2fa when you setup an authenticator app";
return RedirectToPage("./TwoFactorAuthentication");
}
We used the _logger object from the dependency injection (DI) to call the LogError method, which writes an error log in the current logging provider.
We have preconfigured our sample Online Banking app to add Windows event logging by calling the ConfigureLogging method. The ConfigureLogging method will create an ILogger object for the Windows EventLog provider. We set the SourceName property of the ILogger object to OnlineBankingApp to identify the logs generated by our sample Online Banking application:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args) .ConfigureLogging(logging =>
{
logging.AddEventLog(eventLogSettings =>
{
eventLogSettings.SourceName = "OnlineBankingApp";
});
})
We also have configured informational logging by adding an entry in the Logging section of the appsettings.json file. This will create an OnlineBankingApp category with its log level set to Information:
{
"Logging": {
"EventLog": {
"LogLevel": {
"Default": "Warning",
"OnlineBankingApp": "Information"
}
},
With these settings in place, we can now use the Windows EventLog provider for our logging. The instance of the ILogger object, _logger, is already made available through DI. We now simply make a call to the ILogger object's LogError method in the lines of code where a critical exception occurred.
Note
It is important to note that the location and how the logs are stored are essential criteria. The configuration or the code shouldn't place the logs in the same location as the web server. We must implement proper access control to prevent unauthorized viewing of logs. There are open source and enterprise security solutions that provide tools to view, collect, and store logs securely.
Basic DB transactions such as creating, reading, and deleting records are essential to have audit trails, especially when an error occurs as a DB function is performed.
In this recipe, we will fix the insufficient logging of a failed DB transaction, when a related exception is thrown.
Let's take a look at the steps for this recipe:
code .
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid){
return Page();
}
_context.Attach(Backup).State = EntityState.Modified;
try{
await _context.SaveChangesAsync();
}
catch (DbUpdateException){
if (!BackupExists(Backup.ID)){
return NotFound();
}
else{
throw;
}
}
return RedirectToPage("./Index");
}
However, DB operations such as performing a backup and updating its related records should be logged.
public class EditModel : PageModel
{
private readonly OnlineBankingApp.Data .OnlineBankingAppContext _context;
private readonly ILogger<EditModel> _logger;
// code removed for brevity
public EditModel(OnlineBankingApp.Data .OnlineBankingAppContext context, ILogger<EditModel> logger)
{
_logger = logger;
_context = context;
}
try{
await _context.SaveChangesAsync();
}
catch (DbUpdateException ex){
if (!BackupExists(Backup.ID)){
_logger.LogError("Backup not found");
return NotFound();
}
else{
_logger.LogError($"An error occurred in
backing up the DB { ex.Message } ");
throw;
}
}
Adding these lines of code will write an error log in the EventLog logging provider using the same logging settings that were explained in the preceding recipe.
During a sensitive DB operation such as a DB backup, an unexpected error can occur. Such exceptions may cause a faulty system or—worse—an attack in our sample Online Banking app. We mitigate the risk of losing data integrity by keeping track of these DB operations and knowing when an event happened. We make a call to the LogError function to write a log into the Windows event log:
if (!BackupExists(Backup.ID)){
_logger.LogError("Backup not found");
return NotFound();
}
else{
_logger.LogError($"An error occurred in backing up the DB { ex.Message } ");
throw;
}
As a best practice, we provide an appropriate error log message for specific exceptions (DbUpdateException) that we anticipate and, at the most, log only the Message property of the generic exception, avoiding revealing sensitive information in the logs that we create (more about best practices on exception handling in Chapter 13, Best Practices).
As we learned in Chapter 4, Sensitive Data Exposure, ensuring you prevent the exposure of personal details is the key to keeping your application secure, and the same goes for logging information. While logs are helpful, there is also a risk involved in logging excessive data. Perpetrators will find ways to get useful information, and the log store is one source they will try to discover.
In this recipe, we will fix the excessive logging of information such as usernames and passwords.
Let's take a look at the steps for this recipe:
code .
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)
{
_logger.LogInformation($"Customer with email { Input.Email } and password { Input.Password } logged in");
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)
{
_logger.LogInformation("User logged in.");
if (string.IsNullOrEmpty(HttpContext .Session.GetString(SessionKeyName)))
{
HttpContext.Session.SetString (SessionKeyName, Input.Email);
}
Refactoring the code to remove sensitive information such as usernames and passwords prevents an incident where a perpetrator can get hold of the log store and use the information gathered to exploit our sample Online Banking app.
We can view the logs generated by our sample Online Banking app by opening the Windows Event Viewer via the Run command. Here are the steps to do this:
The informational logs you see in Event Viewer are the records generated by the LogInformation method call in the preceding recipe. We have modified the code to prevent explici.t logging of sensitive information such as a user's credentials. We use a generic informational message to remediate the issue of exposing details we would not want anyone to misuse.
Monitoring allows us to actively observe events that occur in our ASP.NET Core web applications. Missing out on incidents as they happen in real time can lead to an attacker causing more damage as each minute goes by. Developers must enable monitoring in their ASP.NET Core web applications to have early preventive detection.
In this recipe, we will fix a lack of security monitoring in our sample Online Banking web app by implementing Azure Application Insights.
Let's take a look at the steps for this recipe:
code .
dotnet add package Microsoft.ApplicationInsights.AspNetCore
public void ConfigureServices(IserviceCollection services)
{
services.AddApplicationInsightsTelemetry();
// code removed for brevity
The AddApplicationInsightsTelemetry method, as the name implies, will add an Insights telemetry collection to our sample Online Banking app.
{
"ApplicationInsights": {
"InstrumentationKey": "My-Instrumentation-Key"
},
"Logging": {
"EventLog": {
"LogLevel": {
"Default": "Warning",
"OnlineBankingApp": "Information"
}
},
Note
To generate an instrumentation key, follow the Create an Application Insights resource instructions in the Microsoft official online documentation for Azure Monitor, found at https://docs.microsoft.com/en-us/azure/azure-monitor/app/create-new-resource.
dotnet run
Incoming requests will now be collected by the Application Insights SDK, including the ILogger logs with Medium, Error, and Critical severity.
We implement telemetry collection by integrating our sample Online Banking web application with Azure Application Insights. Application Insights collects more than performance metrics and logs generated by our ILogger provider—it also performs analysis and application security detection. This cloud-based service sends alerts and notifications in the event of a security issue such as an unsecured form, insecure Uniform Resource Locator (URL) access, and shady user activity.
The preceding steps for the recipe are there to enable the telemetry collection from the server side. Here are the steps to enable monitoring from the client side:
@using OnlineBankingApp
@namespace OnlineBankingApp.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@inject Microsoft.ApplicationInsights.AspNetCore.JavaScriptSnippet JavaScriptSnippet
...
<link rel="stylesheet" href="~/lib/bootstrap/ dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" />
@Html.Raw(JavaScriptSnippet.FullScript)
</head>
By placing the JavaScript in the _Layout.cshtml file, we enable client-side monitoring on all pages that use the layout template of our sample Online Banking web app.
18.218.218.230