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.
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:
Then, let’s put some values in it:
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
:
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:
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:
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’ ‘:
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:
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:
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`
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.