Necessary .NET Security features for securing your web applications

Necessary .NET Security features for securing your web applications

It is extremely necessary to secure your .NET applications before you get hacked. To apply best securities, you have to includes secure coding techniques, anticipate risks and perform security checkups at regular intervals. Fortunately, .NET has lots of security API which you can include in your app to make it more robust. In this tutorial we are going to take a detailed look at the necessary .NET security features which you must apply.

There are 5 common types of security areas to look when developing apps:

  1. Overposting
  2. Store app secrets with Secret Manager
  3. Cross-Site Request Forgery (XSRF/CSRF)
  4. Cross-Site Scripting (XSS)
  5. Open redirect attacks
  6. SQL Injection

Let us now see each of them one by one.

OverPosting Scenario during CREATE & UPDATE records

OverPosting is an attack carried on Model Binding technique so that the attacker is able to set a value to a field which you don’t want to happen. Take for example – a Job Portal has a job application page where teachers can apply for teaching jobs. This page calls a Create Action method to create a Teacher entry in the database, on the submission of the job application form.

The application form is:

@model Teacher
  
<h2>Teacher Application Form</h2>
  
<form asp-action="Create" method="post">
    <div class="form-group">
        <label asp-for="Name"></label>
        <input asp-for="Name" class="form-control" />
    </div>
    <div class="form-group">
        <label asp-for="DOB"></label>
        <input asp-for="DOB" class="form-control" />
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>
</form>

This form has just 2 fields “Name” and “DOB” which the applicant has to fill and submit.

The Teacher.cs class to which this form binds to is:

public class Teacher
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime DOB { get; set; }
    public bool? Accepted { get; set; }
}

Note that the class has an extra bool field Accepted which we have not included on the form. The recruiter will fill this field value to true or false, from some other page like from admin panel of the Job Portal, only after interviewing the teachers and depending upon whether the teacher passes the interview or not. That means the Accepted field should remain null whenever a teacher submits his/her job application. Later on this field will be set to either true or false by the recruiter.

The Create action method which does the creation of teacher’s record in the database is:

[HttpPost]
public IActionResult Create(Teacher teacher)
{
    context.Add(teacher);
    await context.SaveChangesAsync();
    return View();
}

It seems the whole code is OK but the attacker can still successfully launch an Overposting attack. The attacker can use a tool like Fiddler, Postman, or write some JavaScript, to submit the form and create a teacher entry in the database that has “Accepted” field set as “true”. The trick here is that the model binder would pick up that “Accepted” form value (which the attacker has set) and use it to create the Teacher entity. The same thing can be done during update records procedure also.

That’s certainly impossible – A cat “Accepted” for a job of a teacher.

via GIPHY

Overposting with Postman

In Postman we can perform Overposting by selecting “POST” type request. Then select Body > form-data and here put the teachers fields that you want to insert to the database.

See in the below image, I have added Accepted field with value “true” in the form-data. Now when I click Send button, the entry will be added to the database.

Overposting Example

Now check the database to confirm the “Accepted” field is also added with true value. In the below image I have shown this thing.

Overposting Database Entry

Now let’s find out how to prevent this problem.

How to prevent Overposting in ASP.NET Core

Overposting during Creating new records can be prevented by the use of [Bind] attribute. It can be applied to the class to specify which model property should be included in model binding process. Here we want only the “Name” and “DOB” property to be included in Model binding (and not “Accepted” property). Therefore, we can update the Create action with the [Bind] attribute as shown below:

[HttpPost]
public IActionResult Create([Bind("Name", "DOB")]Teacher teacher)
{
    context.Add(teacher);
    await context.SaveChangesAsync();
    return View();
}

The [Bind] attribute doesn’t work well in edit scenarios for preventing overposting because excluded properties are set to null or a default value instead of being left unchanged. In edit scenarios we read the entity from the database first and then call TryUpdateModel method, passing in an explicit allowed properties list.

The TryUpdateModel method is used during the Update action method below. Note that in this method we have not added the Accepted field and so overposting attack will now be prevented.

[HttpPost]
public IActionResult Update(int teacherId)
{
    // fetching teacher record from DB
    var teacherToUpdate = await _context.Teachers.FirstOrDefaultAsync(s => s.Id == teacherId);
    // updating only Name and DOB of the teacher. The “Accepted” field cannot be updated by the attacker.
    if (await TryUpdateModelAsync<Teacher>(
        teacherToUpdate,
        "",
        s => s.Name, s => s.DOB))
    {
        try
        {
            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(Index));
        }
        catch (DbUpdateException /* ex */)
        {
            //Log the error
            ModelState.AddModelError("", "Unable to save changes. Try again later");
        }
    }
}
We can also use ViewModels to prevent overposting attacks.

We can define a TeacherViewModel.cs which will work in Model Binding. Notice this class does not has Accepted field.

public class TeacherViewModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime DOB { get; set; }
}

Now in the controller we can use our view model as shown below.

[HttpPost]
public IActionResult Create(TeacherViewModel teacher)
{
    // insert view model's data to database
}

Safe storage of app secrets

We should not save app secrets in appsettings.json file instead should use Secret Manager Tool to store them. This tool stores the secrets on a separate location from the project, in a file called secrets.json. The most common example is storing database password with Secret Manager Tool.

The Secret Manager tool should only be used in development scenario. In production, use Azure Key Vault to store all the app secrets.

Storing Database Passwork with Secret Manager

Let us now understand how to use Secret Manager Tool. So first run init command from the directory of the project file (.csproj).

dotnet user-secrets init

The command adds UserSecretsId, which is a GUID element, within a PropertyGroup of the project file.

<PropertyGroup>
  <TargetFramework>net6.0</TargetFramework>
  <UserSecretsId>ad0f2959-b89d-45e7-bdae-2b36ca748ae7</UserSecretsId>
</PropertyGroup>

The UserSecretsId is unique to the project and now all your app secrets will be associated with this id. Right click on the project in solution explorer and select Edit Project File to see the UserSecretsId added there.

Now run the following command to store the database password.

dotnet user-secrets set "DbPassword" "joker@123"

The database password is stored in secrets.json file. You can open this file by right clicking the project and select Manage User Secrets.

Manage User Secrets Visual Studio

You can edit this json file to add, edit or remove new secrets. There are also separate commands to perform each of the tasks.

Now go to the database connection string stored in appsettings.json file and remove the password Password=joker@123 from it.

{
  "ConnectionStrings": {
    "Students": "Server=(localdb)\\mssqllocaldb;Database=Student;User Id=yogi;MultipleActiveResultSets=true"
  }
}

Next, in the Program.cs, use the SqlConnectionStringBuilder object’s Password property to add it to the connection string as shown below.

var conStrBuilder = new SqlConnectionStringBuilder(builder.Configuration.GetConnectionString("Students"));
conStrBuilder.Password = builder.Configuration["DbPassword"];
var connection = conStrBuilder.ConnectionString;

// setting connection string for Entity Framework Core
builder.Services.AddDbContext<CompanyContext>(options =>
  options.UseSqlServer(builder.Configuration.GetConnectionString(connection)));

Other important commands:

  • Lists all the secrets – dotnet user-secrets list
  • Remove all secrets – dotnet user-secrets clear

Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks

In Cross-site request forgery (also known as XSRF or CSRF) an attacker manipulate a user into executing malicious actions without their awareness. Take for example, you are currently logged in to your Net Banking account. Now you receive an email stating that you are a lucky winner of a million $ Ferrari car. Seeing this you click the email link and reaches a malicious website.

In this website there is a small form asking for your name and address only. You think it’s Ok as no credit card or bank account they are asking.

XSRF CSRF attack form

This form’s html code is given below.

<h1>Congratulations! Get your Ferrari<h1>
<form action="https://www.bankofamerica.com/api/account" method="post">
    <input type="hidden" name="Transaction" value="withdraw" />
    <input type="hidden" name="Amount" value="1000000" />
    <input type="submit" value="Collect your car now!" />
    ... other fields for name, email, address, etc
</form>

See the form action targets bank of America /api/account endpoint where $100000 amount is added for transaction. Once you click on the Collect your car now! button, the transaction will happen.

But how it is possible? The reason is you are currently logged in to your bank’s website. The bank has already authenticated you and their session’s cookie is residing on your browser. When you click on the Collect your car now! button on the attacker’s website, this bank cookie is attached to the api request and the transaction happens. The bank has no way to identity this api request is not coming from their website.

The above example is using HTTP POST version of this attack. The GET version can also be used where the attacker will make you click an image which will make a GET request.

In order to safeguard yourself from CSRF attacks follow these steps:

.
  1. Never click links from fishy emails.
  2. Sign-off from the apps when your work is done.

Things to remember when building apps in ASP.NET Core:

  1. Never change state on a GET request – for example don’t perform transaction on a HTTP GET request. This will stop attacks when user click on images given on malicious emails.
  2. Implement Token-based authentication like JWT in your apps.
  3. Implement ASP.NET Core Identity in your apps.
  4. Shared hosting environments are vulnerable to CSRF. Subdomains trusts each other cookies which allows CSRF attacks. So host subdomains on different domains.

It is also required to apply [ValidateAntiForgeryToken] filter to your apps most important action methods. The .NET will generate a hidden Verification Token which will be added and verified on each user request to the app and so you are safeguarding them from cross-site request forgery (CSRF) attacks.

In the below code we have applied [ValidateAntiForgeryToken] on the action method and rest .NET will take care.

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Transaction(User user)
{
    // some code
}

Prevent Cross-Site Scripting (XSS)

Cross-Site Scripting (XSS) attacks are caused when malicious scripts (mainly JavaScript) are injected to trusted websites. So when other users load the affected pages, these scripts run, and the attacker is able to steal user cookies, tokens, perform DOM manipulation, or redirect the users to another page.

You can prevent XSS attacks by:

  1. Perform Input validation of data submitted from Forms.
  2. Encode untrusted data before outputting to a page. It’s of 2 types – HTML encoding & JavaScript encoding.
  3. Encode untrusted data before putting it into a URL query string. It’s called by the name URL Encoding.

Encoding in ASP.NET Core

HTML encoding in MVC views (.cshtml) and Razor Pages are done automatically. Example:

@{
    var untrustedInput = "<\"abc\">";
}

@untrustedInput

It will output:

&lt;&quot;abc&quot;&gt;

The characters <, \, ", \,> are encoded since they are used in XSS attacks.

In MVC views and Razor pages the JavaScript encoding is done by placing the value in a data attribute of a tag and retrieve it in your JavaScript. Example:

@{
    var untrustedInput = "<script>alert(1)</script>";
}

<div id="injectedData" data-untrustedinput="@untrustedInput" />

<div id="scriptedWrite" />

<script>
    var injectedData = document.getElementById("injectedData");

    var untrustedInput = injectedData.dataset.untrustedinput;

    document.getElementById("scriptedWrite").innerText += untrustedInput;

</script>

Do not use document.write() on dynamically generated data as it can lead to XSS.

In MVC controllers we can inject HTML, JavaScript and URL encoders object (located on the System.Text.Encodings.Web namespace) on the constructor and then encode the values with the Encode() method.

public class HomeController : Controller
{
    HtmlEncoder he;
    JavaScriptEncoder je;
    UrlEncoder ue;

    public HomeController(HtmlEncoder htmlEncoder,
                          JavaScriptEncoder javascriptEncoder,
                          UrlEncoder urlEncoder)
    {
        he = htmlEncoder;
        je = javascriptEncoder;
        ue = urlEncoder;
    }
    
    public IActionResult EncodeExample()
    {
        var example = "I am <, > a hacker.";
        var encodedValueHTML = he.Encode(example);
        var encodedValueJS = je.Encode(example);
        var encodedValueURL = ue.Encode(example); 
    }
}

Never rely on validation alone, perform encoding of untrusted data before outputting it.

Prevent open redirect attacks

Open Redirect Attack are meant to steal user’s credentials on secured web apps like their bank account username and password, office credentials, etc. In this attack the hacker will convince the user to click a link to a perfectly OK website. But he adds a returnUrl query string parameter to the url. This parameter targets a malicious site where they will be shown a login form to enter their credentials. You know what will happen next.

Example – An office employee ‘Sara’ logs to her account on her office website every day in the morning to get messages from her colleagues. The office website has an address – https://www.myoffice.com. The hacker will now try to get Sara’s username and password so he sends an email with a link saying “Sara, you have important office messages”. But to this link he adds a returnUrl query string as shown below:

 https://www.myoffice.com?returnUrl=https://www.myoffice1.com

The querystring targets a hacker site. So, when Sara click the link, she first reaches her office website (myoffice.com) which redirect her to the url specified in the returnUrl parameter i.e. myoffice1.com. Clearly see the hacker’s website has a very similar name with “1” added to it.

Why this redirection happens? All websites redirect un-authenticated users to login page and they store original requested url in some querystring parameter like ‘returnUrl’. After the user is authenticated, they’re redirected to the URL contained in this parameter. ASP.NET Core Identity does the same redirection thing by having returnUrl to store requested url.

The hackers website design is totally same to the office website. This website also has a login form where Sara enters her username and password. That’s it, the hacker gets her credentials and now she is redirected to her original office website.

The office.com website ask here to login. She believes that her first attempt to log in failed and that her second attempt is successful. The attack was successful.

In open redirect attacks the user most likely remains unaware that their credentials are compromised.

Use LocalRedirect method

Use LocalRedirect method of .NET to Prevent open redirect attacks. The LocalRedirect() method will throw an exception if a non-local URL is specified. Otherwise, it performs the redirect. Example is given below:

public IActionResult Login(string redirectUrl)
{
    //login code
    return LocalRedirect(redirectUrl);
}

You can also use IsLocalUrl method to check whether a URL is local before redirecting.

if (Url.IsLocalUrl(returnUrl))
{
    return Redirect(returnUrl);
}

Stop SQL Injections

In SQL Injection attack the attacker injects malicious code in SQL statements which when executed will damage your database or steal data from database. Let us understand it with an example.

Example: A text box for entering userid is given on a website. It will provide user’s details from the database.

<form type="post">
Enter User Id: <input type="text" name="userId">
<input type="submit" value="Submit">
</form>

The SQL query that executes when the form is submitted:

"SELECT * FROM UserTable WHERE UserId = " + txtUserId;

The user enters user id ‘10’ in the text box and he receives the details of the user. Here everything works well.

Now if user enters 10; DROP TABLE UserTable. Then the query will drop the “UserTable” from the database. Why? because the query will become.

SELECT * FROM UserTable WHERE UserId = 10; DROP TABLE UserTable
Appending 1=1 to SQL Queries

In SQL, 1=1 is always true so if we append it to queries then we will get back result even if we provide wrong inputs. Suppose there is a form which provides super admin all the usernames and passwords of the app. However the super admin has to enter his very secret password.

<form type="post">
Enter Super Admin Password: <input type="text" name="password">
<input type="submit" value="Submit">
</form>

The SQL query executing when correct password is provided is:

SELECT * FROM Users WHERE SuperAdminPass = "*o*D~I/gpKl*?P]"

A hacker can never guess this strong password so he applied 1=1 trick. He enters “abc OR 1=1” to the password textbox. Now the query that will be executing is:

SELECT * FROM Users WHERE SuperAdminPass = "abc" OR 1=1

That it, with 1=1 in the OR clause the SQL query will return all the users from the table even though the password is wrong.

Example – SQL Injection Login form with 1=1

We can log-in to secured websites with 1=1 SQL Injection trick provided the website uses plain SQL Queries to check for user’s credentials in the database. Let us create an example in .NET to understand this.

We have a login form which looks like:

ASP.NET Core Login Form SQL Injection

The login view razor code is:

<div class="text-center">
    <h1 class="display-4">Login Form</h1>
    <form method="post">
        <h1 class="h3 mb-3 fw-normal">Please sign in</h1>

        <div class="form-floating">
            <input type="text" class="form-control" name="username" placeholder="Username">
            <label for="floatingInput">Username</label>
        </div>
        <div class="form-floating">
            <input type="text" class="form-control" name="password" placeholder="Password">
            <label for="floatingPassword">Password</label>
        </div>
        <button class="w-100 btn btn-lg btn-primary" type="submit">Sign in</button>
    </form>
</div>

We check the user’s credentials present in the database in the controller’s action method whose code is given below:

[HttpPost]
public IActionResult Login(string username, string password)
{
    int count;
    string connectionString = Configuration["ConnectionStrings:DefaultConnection"];
    using (SqlConnection connection = new SqlConnection(connectionString))
    {
        string sql = $"Select count(*) from Users where username='{username}' AND Password = '{password}'";
      
        using (SqlCommand command = new SqlCommand(sql, connection))
        {
            connection.Open();
            count = (int)command.ExecuteScalar();
            connection.Close();
        }
    }

    if (count == 0)
    {
        // login failed
        return View();
    }
    else
    { 
        // login successful
        return RedirectToAction("Secured");
    }
}

In the above code plain sql query is executed – string sql = $"Select count(*) from Users where username='{username}' AND Password = '{password}'";. This is the main problem for exploitation.

See the if – else block where the checking is done i.e. if count(*) is 0 then login fails since no user is found in the database. The else block executes when login is successful and the user is redirected to secured area of the website.

if (count == 0)
{
    // login failed
    return View();
}
else
{
    // login successful
    return RedirectToAction("Secured");
}

Let’s now try to login with a random username ‘test’ and password ‘test’.

ASP.NET Core Login

Login fails since no user is found in the database. See the below image where if block is executed.

ASP.NET Core Login fails

It seems login feature is working correctly. Now we will login by entering ' or 1=1 -- on username textbox and anything on password.

SQL Injection 1=1 example

When we enter ' or 1=1 -- the -- is SQL Comment which will prevent further statements from executing and so the SQL query that will be executing becomes:

Select count(*) from Users where username='' or 1=1 --' AND Password = 'abc'

After -- everything is skipped so password is not checked i.e. we are successful in doing login. See the below image where the else block is executed.

SQL Injection 1=1 example

Parameterized Queries to prevent SQL Injection

In ASP.NET core we can prevent SQL injection attacks by using stored procedures, or parameterized queries. The below code is an example of Inserting records with parameterized query.

[HttpPost]
public IActionResult Login(string username, string password)
{
    int count;
    string connectionString = Configuration["ConnectionStrings:DefaultConnection"];
    using (SqlConnection connection = new SqlConnection(connectionString))
    {
        string sql = $"Select count(*) from Users where username=@Username AND Password = @Password";

        using (SqlCommand command = new SqlCommand(sql, connection))
        {
            command.Parameters.Add("@Username", SqlDbType.VarChar).Value = username;
            command.Parameters.Add("@Password", SqlDbType.VarChar).Value= password;

            connection.Open();
            count = (int)command.ExecuteScalar();
            connection.Close();
        }
    }

    if (count == 0)
    {
        // login failed
        return View();
    }
    else
    { 
        // login successful
        return RedirectToAction("Secured");
    }
}

Here we changed the SQL Query by using @Username and @Password parameters.

string sql = $"Select count(*) from Users where username=@Username AND Password = @Password";

We define them in the code as.

command.Parameters.Add("@Username", SqlDbType.VarChar).Value = username;
command.Parameters.Add("@Password", SqlDbType.VarChar).Value= password;

And this will prevent SQL Injection.

Conclusion

I hope you now know some good Security techniques to apply in your .NET applications to make them more secure and prevent attacks. If you like this tutorial kindly share it with your .NET developer friends.

SHARE THIS ARTICLE

  • linkedin
  • reddit

ABOUT THE AUTHOR

I am Yogi S. I write DOT NET artciles on my sites hosting.work and yogihosting.com. You can connect with me on Twitter. I hope my articles are helping you in some way or the other, if you like my articles consider buying me a coffee - Buy Me A Coffee

Leave a Reply

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

Related Posts based on your interest