The injection attack is the most critical web application security threat as per OWASP Top 10 list. In this article, we are going to look at the Injection attack in detail.

To download the source code for this article, visit the OWASP – Injection GitHub Repo.

To see all the articles from this series, visit the OWASP Top 10 Vulnerabilities page.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

What is an Injection Attack

Attackers can perform an injection attack in a web application by sending untrusted data to a code interpreter through a form input or some other mode of data submission. 

For example, an attacker could enter SQL database script into a form that expects plain text. If we do not properly validate the form inputs, this would result in that SQL code being executed in the database. This is the most common type of injection attack and is known as an SQL injection attack.

The Injection Attack Vulnerability

So, when does an application become vulnerable to an injection attack?

An application is vulnerable to an injection attack when it

  • does not validate or sanitize user-supplied data  
  • executes dynamic queries or non-parameterized statements directly in the database
  • uses user-supplied data directly or concatenate it in dynamic queries, commands, or stored procedures

An Example Injection Attack Scenario

Now, let’s take a look at how an injection attack can surface on a poorly designed application.

Designing the Database

For that, We are going to design an application that authenticates users against a database.

First, let’s create a database table for storing Login details:

table

Then, let’s put some values in it:

data

Designing the Application

After that, let’s create an ASP.NET Core Razor Page application.

Creating the Login Model

First, we need to create a Login model:

public class Login
{
    public int ID { get; set; }

    public string Username { get; set; }

    public string Password { get; set; }

    public string Message { get; set; }
}

Then, we need to add two pages – Login & LoginSuccess

Creating the Login Page

We are going to create  Login page with two text inputs for Username and Password:

@page
@model OWASPTop10.Pages.LoginModel
@{
    ViewData["Title"] = "Login";
}

<form method="post">
    @if (!string.IsNullOrEmpty(Model.Login?.Message))
    {
        <p class="alert-danger">
            @Model.Login?.Message
        </p>
    }
    <div class="row">
        <div class="col-md-4">
            <h4>Login</h4>
            <hr />
            <div class="form-group">
                <label asp-for="Login.Username"></label>
                <input asp-for="Login.Username" class="form-control" />
            </div>
            <div class="form-group">
                <label asp-for="Login.Password"></label>
                <input asp-for="Login.Password" class="form-control" type="password" />
            </div>
            <div class="form-group">
                <button type="submit" class="btn btn-primary">Log in</button>
            </div>
        </div>
    </div>
</form>

In the page model class, we’ll write the logic to check the user credentials against the database:

public class LoginModel : PageModel
{
    [BindProperty]
    public Login Login { get; set; }

    public void OnGet()
    {
    }

    public IActionResult OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        using (SqlConnection sqlConnection = new SqlConnection("Data Source=.;Initial Catalog=MvcBook;Integrated Security=True"))
        {
            string commandText = "SELECT [UserName] FROM dbo.[Login] WHERE [Username] = '"
                + Login.Username
                + "' AND [Password]='"
                + Login.Password
                + "' ";
            try
            {
                using (SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection))
                {
                    sqlConnection.Open();
                    if (sqlCommand.ExecuteScalar() == null)
                    {
                        // Invalid Login
                        Login.Message = "Invalid Login.";
                        return Page();
                    }

                    // Valid Login
                    string Username = sqlCommand.ExecuteScalar().ToString();
                    sqlConnection.Close();
                    return RedirectToPage("./LoginSuccess", new { username = Username });
                }
            }

            catch (Exception ex)
            {
                Login.Message = ex.Message;
                return Page();
            }
        }
    }
}

Here, we have built the SQL query by concatenating the user inputs. Then, this query is executed against the database.

Creating the LoginSuccess Page

Let’s add a LoginSuccess page so that users can be redirected to it after a successful login:

@page "{userName}"
@model OWASPTop10.Pages.LoginSuccessModel
@{
    ViewData["Title"] = "LoginSuccess";
}

<h1>Login Success</h1>
<div>Hello, <b>@Model.Username</b> </div>

We also need to add the page model :

public class LoginSuccessModel : PageModel
{
    public string Username { get; set; }

    public void OnGet(string username)
    {
        Username = username;
    }
}

Now, we are going to test the application.

Testing the Application

Let’s run the application and navigate to /Login:

login page

First, we are going to enter valid credentials and check the result:

Username: admin

Password: admin@123

We will be redirected to the login success page:

login success

The application builds the following query and executes at run time:

SELECT [Username] 
FROM   dbo.[Login] 
WHERE  [Username] = 'admin' 
       AND [Password] = 'admin@123'

Now, let’s go back to the login page.

When an attacker reaches our login page, the first thing he/she may try is entering some random credentials. Then, they’ll be presented with the Invalid login error:

invalid login

The next thing an attacker may do is check if the web application is vulnerable to injection attacks. They may do so by entering some special characters in the password field.

For example, they may try:

Username: admin 

Password: password'

Then, the attacker is presented with the following error message: Unclosed quotation mark after the character string ‘password’ ‘. Incorrect syntax near ‘password’ ‘:

invalid login with exception

Let’s examine the actual query executed at run time:

SELECT [Username] 
FROM   dbo.[Login] 
WHERE  [Username] = 'admin' 
       AND [Password] = 'password''

This is going to make the attacker very happy because he/she discovers that:

  • There are no validations against the user inputs
  • The application concatenates the user input with some database script in the back-end
  • It executes the concatenated string directly against the database

Having learned these facts, the next step the attacker may perform is to modify the input in such a way that the resulting database query always returns true.

For example, they could try:

Username: admin

Password:  ' OR 1=1 --

Then, the application successfully authenticates the attacker and takes him/her to the login success page:

login success

So, how did this happen?

Injection Attack Explained

What happened above is, when our input is concatenated with the SQL query, it always returns true and hence the application passes through the authentication phase. Additionally, it falsely identifies the user as the first user in the database, which unfortunately in most cases will be a user with administrative privileges.

Let’s examine the actual query that the application executes:

SELECT [Username] 
FROM   dbo.[Login] 
WHERE  [Username] = 'admin' 
       AND [Password] = '' 
        OR 1 = 1 --' 

This always returns true and ignores any statements after.

Depending on how the application is designed, how the permission is managed and how the user inputs are used, the attack can get more severe. An unauthorized user can perform the following actions in the increasing order of severity:

  • Log in with administrative privileges
  • Fetch sensitive data from the database
  • Delete important data
  • Drop some key tables

Prevention Steps for Injection Attacks

In the previous section, We have looked at how an injection attack can happen. Now we’re going to see how we can prevent these types of attacks.

Injection Prevention – Validation/Sanitization of User Inputs

Validating/sanitizing the user inputs is the first line of defense against most types of attacks. The kind of validation that we need to perform depends on the application’s logic and the expected user inputs.

In the application we discussed in the above section, let’s introduce a validation to restrict single quotes:

<div class="row">
    <div class="col-md-4">
        <h4>Login</h4>
        <hr />
        <div class="form-group">
            <label asp-for="Login.Username"></label>
            <input asp-for="Login.Username" class="form-control" pattern="[^']*$" title="Cannot contain single quotes"/>
        </div>
        <div class="form-group">
            <label asp-for="Login.Password"></label>
            <input asp-for="Login.Password" class="form-control" type="password" pattern="[^']*$" title="Cannot contain single quotes"/>
        </div>
        <div class="form-group">
            <button type="submit" class="btn btn-primary">Log in</button>
        </div>
    </div>
</div>

This restricts the user from entering single quotes:

validation

We should always remember that an attacker can easily bypass any validations implemented on the client-side. Therefore, we should implement equivalent validations on the server-side as well:

public class Login
{
    public int ID { get; set; }

    public string Username { get; set; }

    [ShouldNotContainSingleQuotesValidation(ErrorMessage = "Cannot contain single quotes")]
    public string Password { get; set; }

    public string Message { get; set; }
}

public sealed class ShouldNotContainSingleQuotesValidation : ValidationAttribute
{
    public override bool IsValid(object value)
    {
        return !value.ToString().Contains("'");
    }
}

The validation shown here just restricts the single quotes. But in the real world, we’ll have to implement more complex validations depending on our application’s context.

Injection Prevention – Parameterized Queries/Stored Procedures/Use of ORMs

As we explained in the previous section, validation is just a first line of defense and we cannot completely rely on just that for our application’s security.

Parameterized Queries

Parameterizing the queries will automatically prevent the injection attempts. Let’s modify the code that authenticates the user:

public IActionResult OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    using (SqlConnection sqlConnection = new SqlConnection("Data Source=.;Initial Catalog=MvcBook;Integrated Security=True"))
    {
        string commandText = "SELECT [UserName] FROM dbo.[Login] WHERE [Username] = @username AND [Password]= @password ";
        try
        {
            using (SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection))
            {
                sqlCommand.Parameters.Add(new SqlParameter("username", Login.Username));
                sqlCommand.Parameters.Add(new SqlParameter("password", Login.Password));

                sqlConnection.Open();
                if (sqlCommand.ExecuteScalar() == null)
                {
                    // Invalid Login
                    Login.Message = "Invalid Login.";
                    return Page();
                }

                // Valid Login
                string UserName = sqlCommand.ExecuteScalar().ToString();
                sqlConnection.Close();
                return RedirectToPage("./LoginSuccess", new { username = Username });
            }
        }

        catch (Exception ex)
        {
            Login.Message = ex.Message;
            return Page();
        }
    }
}

Now, let’s try to login with below credentials:

Username: admin

Password: password`

invalid login

By parameterizing the user inputs, we can see that the injection attacks are taken care of by the ADO.Net.

Stored Procedures

We can improve this one step further by changing inline SQL queries with a stored procedure.

Let’s create a stored procedure and encapsulate the logic for checking user credentials there:

CREATE PROCEDURE [dbo].[CheckLogin]
	@username varchar(50),
	@password varchar(50)
AS
	SELECT [Username] 
	FROM dbo.[Login] 
	WHERE 
		[Username] = @username 
		AND [Password]= @password 
RETURN 0

We also need to make the following changes in code:

public IActionResult OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    using (SqlConnection sqlConnection = new SqlConnection("Data Source=.;Initial Catalog=MvcBook;Integrated Security=True"))
    {
        string commandText = "[dbo].[CheckLogin]";

        try
        {
            using (SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection))
            {
                sqlCommand.CommandType = CommandType.StoredProcedure;

                sqlCommand.Parameters.Add(new SqlParameter("@username", Login.Username));
                sqlCommand.Parameters.Add(new SqlParameter("@password", Login.Password));

                sqlConnection.Open();
                if (sqlCommand.ExecuteScalar() == null)
                {
                    // Invalid Login
                    Login.Message = "Invalid Login.";
                    return Page();
                }

                // Valid Login
                string UserName = sqlCommand.ExecuteScalar().ToString();
                sqlConnection.Close();
                return RedirectToPage("./LoginSuccess", new { username = Username });
            }
        }

        catch (Exception ex)
        {
            Login.Message = ex.Message;
            return Page();
        }
    }
}

This will help us to prevent injection attacks and achieve better separation between the SQL scripts and user inputs.

Use of ORMs

Another better alternative would be to use Object Relational Mapping (ORM) frameworks to make the data access more seamless. Using ORM tools also means that developers rarely have to write SQL statements in their code and good ORM tools use parameterized statements under the hood. 

Entity Framework (EF) Core is an example of a good ORM tool that works well with .NET Core. We have explained how to use EF Core with ASP.NET Core in the code-first and database-first articles.

Using an ORM does not automatically make us completely immune to SQL injection as they still allow us to construct SQL statements if we intend to do so. Therefore, we should try to avoid that as much as possible and have to be extremely careful if we decide to do so.

Injection Prevention – Apply the Principle of Least Privilege

The Principle of Least Privilege states that we should operate every user or process within a system using the least amount of privilege necessary to undertake their job. That way, we can mitigate any risks if a component is compromised or an individual goes rogue.

Usually, we do not expect an application to change the structure of the database at run-time. Typically we create, modify and drop database objects as part of the release process with temporarily elevated permissions. Therefore, it is a good practice to reduce the permissions of the application at runtime, so that it can at most edit data, but not change the database structures. In a SQL Server database, this means making sure our production accounts can only execute DML statements, not DDL statements.

While designing complex databases, it is worth making these permissions even more fine-grained. We should allow data edits only through stored procedures that run on user accounts with elevated privileges. Furthermore, we should execute all data read/search process with read-only permissions.

Sensibly designing database access permissions this way can provide a vital next line of defense. That way, even if the attacker gets access to our system, we can mitigate the type of damage they can cause.

Injection Prevention – Password hashing

The example attack that we performed earlier relied on the fact that the password was stored as plain-text in the database. In fact, storing unencrypted passwords is a major security flaw in itself. Applications should store user passwords as strong hashes, preferably salted. By doing so, we can mitigate the risk of a malicious user trying to steal credentials or impersonating other users.

Injection Prevention – Using Industry Standard Third-Party Authentication

Wherever possible, it’s a good idea to consider outsourcing the authentication workflow of our application entirely. Google, Microsoft, LinkedIn, Facebook, Twitter, etc provide OAuth based authentication, that can be used to let users log in to our website using their existing accounts on those systems. As developers, this saves us the pain of implementing our own authentication mechanism and we can also avoid the risk of storing user credentials. As these providers implement industry-standard protocols, we can rest assured that these systems will have good security measures in place to prevent all common attack scenarios.

Injection Prevention – Setting Limits to Data Exposure

There are a few ways in which we can put limits on the result sets returned by our application.

We can limit row counts processed or returned. By doing so, we can prevent reading or returning too much data. It is also possible to implement a date range limit to restrict the data to be returned from just a narrow range. Also, it’s a good practice to always restrict blank searches. As a rule of thumb, always “return only what the user specifically asks for”.

By implementing these restrictions, we can set a limit on the data exposure that can happen even if an attacker gets access to our application.

Conclusion

In this article, we have learned the following topics:

  • What is an Injection Attack
  • When does our application become vulnerable to Injection Attacks
  • An example Injection Attack scenario
  • The steps for preventing an Injection Attack

In the next article, we are going to talk about Broken Authentication vulnerability.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!