- Security
- A
Catch the vulnerability with your own hands: custom annotations of C# code
I think it is no secret to many that vulnerabilities in a project can have an extremely negative impact on it. There are a number of ways to deal with vulnerabilities, ranging from manual search to the use of specialized tools. One of these tools will be discussed in the article.
A few introductory words
Today we will talk about the mechanism of user annotations in the PVS-Studio static analyzer and how it can help in the fight against vulnerabilities. It is worth noting that the idea of adding user markup to the analyzer is not new. It has already been successfully implemented in the C++ part of PVS-Studio. You can learn about the prerequisites for the appearance of C++ annotations, as well as their capabilities, in a separate article.
The development team of the C# analyzer decided not to lag behind their colleagues and began to implement user annotations for their part of the analyzer.
I have repeatedly used the expression "user annotations". If there should be no problems with understanding the first part of this expression, then the second part should be focused on. By "annotations" in this context, we mean some markup that allows the analyzer to get additional information about the source code. For example, by marking a method, we can tell the analyzer that this method always returns unchecked external data. If such data is not processed accordingly, then when it enters certain parts of the program, it can lead to the appearance of a potential vulnerability.
What is a potential vulnerability? In short, it is a defect contained in the program code that, under certain circumstances, can be exploited by an attacker. You can read more about potential vulnerabilities here.
How can you protect your code from potential vulnerabilities? One of the ways to help solve this problem can be the taint analysis technology. It allows you to track the spread of potentially unsafe data through the program.
Implementation of taint analysis in PVS-Studio
In PVS-Studio, taint analysis is implemented through data-flow analysis, marking sources and sinks of tainted data. During the analysis, the paths of tainted data propagation are calculated. If such data reaches a so-called sink without verification, the analyzer will issue a warning about a possible security defect. This was a demo version of the description of taint analysis in PVS-Studio, but there is also a full one :)
Currently, there are 16 taint diagnostics in the analyzer, each responsible for a specific security defect:
V5608 — SQL injection;
V5609 — Path traversal vulnerability;
V5610 — XSS vulnerability;
V5611 — Insecure deserialization vulnerability;
V5614 — XXE vulnerability;
V5615 — XEE vulnerability;
V5616 — Command injection;
V5618 — Server-side request forgery;
V5619 — Log injection;
V5620 — LDAP injection;
V5622 — XPath injection;
V5623 — Open redirect vulnerability;
V5624 — Configuration vulnerability;
V5626 — ReDoS vulnerability;
V5627 — NoSQL injection;
V5628 — Zip Slip vulnerability.
Let's consider an example of a potential vulnerability found by the analyzer:
void ProcessRequest(HttpRequest request)
{
string name = request.Form["name"];
string sql = $"SELECT * FROM Users WHERE name='{name}'";
ExecuteReaderCommand(sql);
....
}
void ExecuteReaderCommand(string sql)
{
using (var command = new SqlCommand(sql, _connection))
{
using (var reader = command.ExecuteReader()) { .... }
}
....
}
In the variable name, the user name obtained from an external source will be recorded. Subsequently, the value of this variable becomes part of the SQL query.
This code is vulnerable to SQL injection because it uses unchecked external data to form a database query.
For example, the name parameter may contain the following value:
'; DELETE FROM Users WHERE name != '
When this string is substituted into the query template, the following SQL command will be obtained:
SELECT * FROM Users WHERE name='';
DELETE FROM Users WHERE name != ''
Executing such a query will result in the deletion of all records from the Users table (provided that a value is set for the name column for each user).
This potential vulnerability will be found using PVS-Studio.
User Annotations
Note. At the time of writing, user annotations are implemented only for taint analysis in the C# part of the analyzer. This markup method is required for GOST R 71207–2024. Now PVS-Studio provides users with the ability to mark sources (source procedures) and sinks (sink procedures) of sensitive data. It is worth noting that in the future we plan to expand the capabilities of user C# annotations (annotations not only for taint analysis).
Earlier, I already wrote about what is meant by "annotation" in the current context, but just in case, I will repeat. An annotation is some markup that allows the analyzer to get additional information about the source code.
In PVS-Studio, starting from version 7.33, a mechanism has appeared that will allow users to mark sources, transmitters, and receivers of unsafe data for C# projects. Thus, the analyzer will be able to more accurately find potential vulnerabilities for a specific project.
It should be clarified that the analyzer already has markup for most popular library methods/constructors/properties. For example, the analyzer already "out of the box" knows that when passing the result of the System.Console.ReadLine method to the System.Data.SqlClient.SqlCommand constructor, an SQL injection may occur.
In short, this works because the analyzer has annotations that say that System.Console.ReadLine is a source of taint data, and System.Data.SqlClient.SqlCommand is a receiver. If taint data gets into it, an SQL injection vulnerability may occur.
Let's return directly to user annotations. For greater clarity, let's consider an example of their use.
Suppose we have the following class in the project:
namespace MyNamespace
{
public class MyClass
{
public string GetUserInput()
{
....
}
public string ModifyCommand(string command, string commandAddition)
{
....
}
public void ExecuteCommand(string command)
{
....
}
}
}
The methods of this class perform the following actions:
GetUserInput returns some user input;
ModifyCommand adds a string to an existing SQL command;
ExecuteCommand executes the SQL command.
Let's also consider a possible use case for these methods:
public static void ProcessUserInput(MyClass test)
{
string userInput = test.GetUserInput();
string modifiedCommand = test.ModifyCommand("I'm a sql command",
userInput);
test.ExecuteCommand(modifiedCommand);
}
In this case, the user input is passed directly to the method executing the SQL command without validation. As mentioned above, this can lead to SQL Injection.
To ensure the analyzer tracks such cases, a series of method annotations need to be added. The content of the annotation file will look as follows:
{
"version": 1,
"language": "csharp",
"annotations": [
{
"type": "method",
"namespace_name": "MyNamespace",
"type_name": "MyClass",
"method_name": "GetUserInput",
"returns": {
"attributes": [
"always_taint"
],
"namespace_name": "System",
"type_name": "String"
}
},
{
"type": "method",
"namespace_name": "MyNamespace",
"type_name": "MyClass",
"method_name": "ModifyCommand",
"params": [
{
"namespace_name": "System",
"type_name": "String"
},
{
"namespace_name": "System",
"type_name": "String",
"attributes": [
"transfer_annotation_to_return_value"
]
}
]
},
{
"type": "method",
"namespace_name": "MyNamespace",
"type_name": "MyClass",
"method_name": "ExecuteCommand",
"params": [
{
"namespace_name": "System",
"type_name": "String",
"attributes": [
"sql_injection_target"
]
}
]
}
]
}
Let's break down the purpose of each annotation:
The annotation of the GetUserInput method contains information that this method returns taint data.
The annotation of the ModifyCommand method contains information that if taint data enters the second argument, the method will also return taint.
The annotation of the ExecuteCommand method indicates to the analyzer that if taint data enters the first argument, SQL Injection may occur.
Thus, the analyzer understands that taint data from GetUserInput is passed to ModifyCommand. After that, the return value of ModifyCommand also becomes tainted. This value is passed to the ExecuteCommand method, which in turn can lead to SQL Injection.
If you add annotations to a project that contains the use of ProcessUserInput, and then analyze this project, you will get the following warning:
V5608 [OWASP-5.3.4, OWASP-5.3.5] Possible SQL injection. Potentially tainted data in the 'modifiedCommand' variable is used to create SQL command.
It is worth noting that you can add the $schema field to the annotation file. Thanks to this field, modern text editors and IDEs can perform validation and suggest possible values while editing the file. You can find out what values the $schema field accepts in the documentation.
You can read more about the capabilities of custom C# annotations in the documentation.
Conclusion
So, we have figured out how annotations can help identify potential vulnerabilities. If you want to annotate your project and see if there are any potential vulnerabilities in it, I suggest trying PVS-Studio.
If you do not want to create annotations, you can always rely on the developers of PVS-Studio. As we mentioned above, many annotations of library methods, constructors, and properties are available immediately after installation. Therefore, you just need to check your project :)
If you want to share this article with an English-speaking audience, please use the translation link: Nikita Panevin. Catch vulnerability on your own: user annotations for C# code.
Write comment