Speaking of Software Vulnerabilities

“You are an explorer, and you represent our species, and the greatest good you can do is to bring back a new idea because our world is endangered by the absence of good ideas. Our world is in crisis because of the absence of consciousness.”
Terence McKenna

I was scrolling on the internet at exactly 1:33 am when I stumbled on an interesting website https://chidiwilliams.com/post/a-refresher-on-software-vulnerabilities/ where he spoke about "A refresher on software vulnerabilities" and he mentioned the Common Weakness Enumeration list, according to Chidi, the CWE (Common Weakness Enumeration) list is something like the "Grammys of security vulnerabilities", and I 100% agree. Personally, it was an eye-opener on how to avoid common mistakes and risks while building software.

You might visit the list and see that a few of the weaknesses are written and applicable to some languages, but I will try to explain some of the vulnerabilities on the list and some that I have personally found using C#.

I will be talking about five random vulnerabilities, but you can stick around till the end of this article to get a bonus software vulnerability and how to avoid it.

Exposure of Sensitive Information to an Unauthorized Actor

Let's say you are building a login page, for instance, which is used to log in to your application (this can be your dashboard, admin area, and so on), the first line of thought is to check the validity of the supplied email and password, next is to verify and notify the user of a successful or failed login.

Where does the exposure of sensitive information come in in this context? How the message is displayed when a user enters the wrong email or password, let us take a look at this code below:

public async Task<IActionResult> Login(string email, string password)
{
  if(ModelState.IsValid){

   var verifyEmail = await _userManager.FindByEmailAsync(email);

   if(verifyEmail != null)
   { 
      var verifyUser = await  _signInManager.PasswordSignInAsync(email,password,false,false);

       if(verifyUser.Succeeded)
       {
          return Ok("login successful");
       }

      ModelState.AddModelError(string.Empty, "your password is incorrect");

   }else{

   ModelState.AddModelError(string.Empty, "email does not exist, try again");

   }
  }
}

When we take a look at the code above, notice how we have different messages for when a wrong email is sent, the same applies to a wrong password. This allows a potentially harmful actor or attacker to discover a valid email by simply trying different values until the incorrect password message is returned. The drawback of this is that it makes it easier for an attacker to obtain half of the necessary authentication credentials.

Please note that these messages can be helpful to a user, but at the same time, it is very much useful to a potential attacker. Instead, we can display the message in a much better way, let us take a look at this code below:

public async Task<IActionResult> Login(string email, string password)
{
  if(ModelState.IsValid){

   var verifyEmail = await _userManager.FindByEmailAsync(email);
   var verifyUser = await _signInManager.PasswordSignInAsync(email,password,false,false);

   if(verifyEmail != null && verifyUser.Succeeded)
   { 
          return Ok("login successful");
   }
   ModelState.AddModelError(string.Empty, "Please check your credentials and try again");

  }
}

Use of Hardcoded Credentials

When trying to leverage the resources of an external service (such as an external API), in most cases, we are usually given a secret/private API key which allows us to be able to use the said service. As a developer, you might be tempted to include the key directly in your code so that you can easily access the service, but this is a security vulnerability.

Hardcoding the key in your code makes it easily accessible to anyone who has access to the code, including attackers. Once the attacker has access to the key, they can use it to gain unauthorized access to the service and steal sensitive information.

Here is a simple mistake that can be made by hardcoding credentials:

var apiKey = "abcdefghijk1234567890lmnopq";
var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
var response = await clientGetAsync("https://externalservice.com/");

To avoid this vulnerability, you should always store the keys and other sensitive information in a secure location, such as environment variables or configuration files that are not accessible to unauthorized users. These keys should also be rotated regularly to prevent unauthorized access.

Here is an example of how you can store the key in an environment variable:

// Get the API key from an environment variable
var apiKey = Environment.GetEnvironmentVariable("API_KEY");
// Use the API key to make a request to the external service
var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
var response = await client.GetAsync("https://externalservice.com/");

By storing the API key in an environment variable, you ensure that it is not accessible to unauthorized users and can be easily rotated when necessary.

Remember that storing sensitive information in plain text in your code is a security vulnerability, and you should always take steps to protect your code and your users.

Null Pointer Dereference

A null pointer dereference occurs when a program attempts to access an object or variable that has a null value. This can result in a crash, In C# and .NET Core, null pointer dereferences are a common problem because of the use of null values. A real-world example of this vulnerability would be a fintech application that uses a null value for a user's account balance.

Let us look at the code below to see how it would have been done before:

public decimal GetAccountBalance(Guid userId)
{
    var user = _dbContext.Users.Find(userId);
    return user.Account.Balance;
}

Let us take a look at how the null pointer dereferences can be handled instead:

public decimal GetAccountBalance(Guid userId)
{
    var user = _dbContext.Users.Include(u => u.Account).FirstOrDefault(u => u.Id == userId);
    return user?.Account?.Balance ?? 0;
}

In the updated code, we have used the null-conditional operator to check if the user or account is null before accessing the balance property. If either the user or account is null, the method will return 0.

Improper Authentication

Improper authentication is a security vulnerability that can cause a lot of harm to your software. This vulnerability arises when an application does not properly verify the identity of a user. For instance, a user could be logged in without the proper authentication credentials or a user could impersonate another user. This can lead to a lot of issues, including data breaches, unauthorized access to sensitive information, and so on.

Here is a way to allow improper authentication of a user:

public async Task<IActionResult> ViewCreditCardData()
{
  // code to show credit card information
}

To avoid improper authentication, ensure that your application verifies the user’s identity before allowing them to access any sensitive information or functionality. Here’s a simple way to get it done example:

[Authorize]
public async Task<IActionResult> ViewCreditCardData()
{
  // code to show credit card information
}

Notice that in the code above, the [Authorize] attribute is used to ensure that the user is authenticated before allowing them to view sensitive data, if the user is not authenticated, a 401 error will be returned and the user will not be able to access the Credit Card data.

Another case is when a user can access a resource or perform an action that they should not be allowed to do. This can occur when the authorization checks are not properly implemented or when the checks are bypassed. A real-world example of this vulnerability would be a Payroll application that allows users to view sensitive employee information without proper authorization.

Let us have a look at an improper way to handle authentication:

[HttpGet]
public async Task<ActionResult> ViewPayrollInfo(Guid employeeId)
{
   var getPayrollInfo = await ViewPayrollDetails(employeeId);
   return View(getPayrollInfo);
}

public async Task<Payroll> ViewPayrollDetails(Guid employeeId)
{
    var payrollInfo= await _dbContext.Payroll.Where(e => e.EmployeeId == employeeId).FirstOrDefaultAsync();
    return payrollInfo;
}

Notice that this way, anybody with or without an "Employee" role can view payroll information. Let's fix this:

[Authorize(Roles = "Employee")]
[HttpGet]
public async Task<ActionResult> ViewPayrollInfo(Guid employeeId)
{
   var getPayrollInfo = await ViewPayrollDetails(employeeId);
   return View(getPayrollInfo);
}

public async Task<Payroll> ViewPayrollDetails(Guid employeeId)
{
    var payrollInfo = await _dbContext.Payroll.Where(e=> e.EmployeeId == employeeId).FirstOrDefaultAsync();
    return payrollInfo;
}

In the updated code, we have added an [Authorize] attribute to the method to ensure that only users with the Employee Role can view their payroll details.

SQL Injection

SQL injection is a common vulnerability in web applications that use SQL databases. It occurs when user input is not properly sanitized before being used in a SQL query, allowing an attacker to inject malicious SQL code into the query.

Let's look at a simple code to show how we can make SQL injection occur in our software:

public async Task<Users>  GetUserInformation(Guid userId)
{
    var sql = $"SELECT * FROM Users WHERE UserId = '{userId.ToString()}'";
    var data = await _dbContext.Users.FromSqlRaw(sql).ToListAsync();
    return data;
}

Now let us see the opposite, I.e handling SQL injection:

public async Task<Users>  GetUserInformation(Guid userId)
{
    var data = await _dbContext.Users
        .FromSqlInterpolated($"SELECT * FROM Users WHERE UserId = {userId.ToString()}")
        .ToListAsync();
    return data;
}

In the example above, we use parameterized SQL queries to ensure that user input is properly sanitized before being used in the query. This prevents SQL injection attacks and ensures the security of our database.

Conclusion

Software vulnerabilities can cause a lot of harm to your software and your users. To avoid these vulnerabilities, ensure that your application properly validates all user input, uses strong encryption techniques, verifies the identity of users, and more. Always keep your software up-to-date and stay informed about the latest security threats and vulnerabilities, you can also look at the Common Weakness Enumeration list from time to time. By doing so, you can help keep your software and your users safe and secure.

Please if you have questions, observations, feedback, or comments please kindly drop them in the comments section, thanks, and happy coding.