﻿# OWASP, уязвимости и taint\-анализ в PVS\-Studio C\#\. Смешать, но не взбалтывать

Мы продолжаем развивать PVS\-Studio как SAST решение\. Одно из направлений, в котором ведутся работы, – улучшение покрытия OWASP\. А какой же OWASP без taint\-анализа? Вот и мы так подумали и решили прикрутить его к C\# анализатору\. О том, что получилось, а что не очень, ниже\.

![0831_TaintAnalysis_Cs_ru/image1.png](https://import.viva64.com/docx/blog/0831_TaintAnalysis_Cs_ru/image1.png)

**Примечание**\. Данная статья не ставит перед собой цель во всех подробностях раскрыть тематику SQL инъекций и работы с SQL из C\#\. Эти вопросы затрагиваются поверхностно и служат цели дополнения рассказа про taint\-анализ\.

## Что за taint\-анализ?

Упрощённо говоря, с помощью taint\-анализа отслеживается распространение по программе данных, полученных из внешнего источника \(taint source\)\. Само по себе распространение "заражённых" данных опасности не несёт\. Однако есть ключевые точки \(taint sinks\), при попадании в которые возможно возникновение различных уязвимостей: SQLI, XSS и т\.п\. Суть taint\-анализа заключается в том, чтобы находить такие трассы распространения данных, по которым они могут без валидации попасть из taint source в taint sink\.

Для реализации нам нужно будет учесть:

* источники заражения \(taint sources\);
* приёмники данных \(taint sinks\);
* способы передачи заражённых данных;
* сброс статуса заражения у данных\.

Смешать, но не взбалтывать\. ;\)

Простенький пример:

```cpp
var userName = Request.QueryString["userName"];
var rawCommand = "SELECT * FROM users WHERE name = '" + userName + "'";
var sqlCommand = new SqlCommand(rawCommand);
// Execution of the tainted command
```

В прямом смысле слова "классический" пример SQL инъекции – если будете гуглить, сразу наткнётесь на что\-нибудь подобное\. Проблема в том, что данные приходят от пользователя и сразу используются для формирования команды\. Как следствие, злоумышленник волен подстроить данные таким образом, чтобы изменить логику SQL\-запроса\. Ожидаете на вход строку вида _JohnSmith_? Получите вместо этого входные данные вида _Sergey'; DROP TABLE users; \-\-_\. Распишитесь\.

## Taint анализ в PVS\-Studio: общее

### С\+\+

Вообще первым анализатором в PVS\-Studio, в котором была реализована концепция taint\-анализа, был анализатор C и C\+\+ кода\. На taint\-анализ опирается диагностическое правило [V1010](https://pvs-studio.ru/ru/docs/warnings/v1010/)\. Оно ищет различные кейсы, базирующиеся на одинаковом принципе: в программу извне поступают данные и без должной проверки приходят туда, куда не должны\. Например, в функцию вызова командного интерпретатора – _system_\. С помощью этого правила удалось найти несколько интересных кейсов, которые я описывал в [отдельной статье](https://pvs-studio.ru/ru/blog/posts/cpp/0599/) \(в ней даже пара видео есть\)\. Ещё на C\+\+ Russia 2018 я делал [небольшой доклад](https://youtu.be/ORygHFn2uJk), в том числе и на эту тему\.

**Примечание**\. Из упомянутой статьи особенно примечательным мне кажется случай, когда в коде исправляли CVE, но что\-то пошло не так\. В итоге код поправили, проблема никуда не ушла, и через какое\-то время ей был присвоен новый CVE\-идентификатор\. Уже после этого код исправили как нужно\. :\)

### C\#

Вообще желание реализовать taint\-анализ в C\# анализаторе было у нас давно\. Наверное, примерно с того самого времени, как он был имплементирован в C\+\+ анализаторе\. Мысли "сделать аналог V1010 в C\# анализаторе" периодически озвучивались, однако руки всё как\-то не доходили\. Но, как я упоминал выше, одно из намеченных направлений развития C\# анализатора в 2021 году – улучшение покрытия OWASP \(в первую очередь – OWASP Top 10 2017, но и про ASVS забывать не хочется\)\. Это стало отличным поводом для того, чтобы руки наконец дошли\.

Первой диагностикой, которая будет использовать taint\-анализ, решили сделать диагностику по поиску возможных SQL инъекций\. Популярная тема, есть упоминание и в OWASP Top 10 2017 \([A1:2017\-Injection](https://owasp.org/www-project-top-ten/2017/A1_2017-Injection)\) и в OWASP ASVS – отличный кандидат\. На том и порешили\.

## Taint анализ в PVS\-Studio C\#: как оно работает?

На базе data\-flow анализа\. В принципе, основная инфраструктура уже была готова\. Что нужно было сделать, так это добавить информацию про источники, приёмники, передачу заражения и сброс заражённого статуса\. 

Параллельно мы дорабатывали анализатор в тех местах, которые находили: улучшили поддержку интерполированных строк в data\-flow, обработку счётчиков циклов, убрали ряд false positives за счёт доработок общих механизмов\. В общем, параллельно подкручивали анализатор то тут, то там\.

Но вернёмся к taint\-анализу\.

### Источники заражённых данных \(taint sources\)

Вся цепочка распространения заражённых данных по коду приложения начинается с источников\. Мы считаем, что источники возвращают заражённые данные безусловно\. В общем и целом, в качестве источников выступают те точки приложения, где данные на вход могут поступать от пользователя\.

Примеры источников:

* HttpRequest\.QueryString;
* TextBox\.Text;
* Console\.ReadLine;
* и т\.п\.

Например, в следующем коде мы считаем переменную _taintedVariable_ заражённой\.

```cpp
void Example()
{
  var taintedVariable = Console.ReadLine();
  TaintSink(taintedVariable);
}
```

В качестве ещё одного типа источников мы приняли параметры методов, доступные внешнему коду\. Например, параметры _public_ методов _public_ классов:

```cpp
public class Helper
{
  public void ExecuteCommand(String commandToExecute)
  {
    TaintSink(commandToExecute);
  }
}
```

В данном случае в методе _ExecuteCommand_ мы считаем параметр _commandToExecute_ заражённым\. Суть такого подхода в следующем\. Если метод доступен для внешней сборки, скорее всего, это API для взаимодействия с библиотекой\. Альтернативный вариант – кто\-то просто не заморачивается по поводу модификаторов области видимости\. :\)

Разработчик, использующий библиотеку, может рассчитывать, что данные проверяются внутри вызываемого метода из библиотеки\. Даже если код библиотеки открыт и лежит где\-нибудь на GitHub, скорее всего, пользователь библиотеки не проверяет имплементацию каждого используемого метода\. В общем, пользователь библиотеки может рассчитывать на то, что данные будут проверены внутри вызываемого метода\.

Разработчик же самой библиотеки может рассчитывать на то, что на вход методу приходят уже проверенные данные\. Значит, в повторной проверке необходимости нет – пришедшие данные можно использовать напрямую\.

Таким образом, может возникнуть ситуация, когда данные, пришедшие от пользователя в приложении, не были проверены ни в самом приложении, ни в используемой им библиотеке\. Как результат — прямое использование внешних данных, что может привести к возникновению уязвимости\.

PVS\-Studio не сможет понять, что стоит за вызовом библиотечного метода, если его исходный код недоступен, а сам метод непроаннотирован\. Однако ситуации такие обнаруживать всё же хочется\. Поэтому, как я писал выше, можно предупреждать самих разработчиков библиотеки о том, что данные из параметра публично доступного метода могут попасть в taint sink без обработки\. 

![0831_TaintAnalysis_Cs_ru/image2.png](https://import.viva64.com/docx/blog/0831_TaintAnalysis_Cs_ru/image2.png)

Рассмотрим какой\-нибудь пример, иллюстрирующий сказанное\.

```cpp
public class DBHelper
{
  public void ProcessUserInfo(String userName)
  {
    ....
    var command = "SELECT * FROM Users WHERE userName = '" + userName + "'";
    ExecuteCommand(command);
    ....
  }

  private void ExecuteCommand(String rawCommand)
  {
    using (SqlConnection connection = new SqlConnection(_connectionString))
    {
      ....
      using (var sqlCommand = new SqlCommand(rawCommand, connection))
      {
        using (var reader = sqlCommand.ExecuteReader())
          ....
      }
    }
  }
}
```

Метод _ProcessUserInfo_ доступен для использования внешнему коду \(_public_ метод в _public_ классе\)\. Параметр _userName_ используется для формирования SQL запроса в строковом представлении \(записывается в переменную _command_\)\. Затем _command_ в качестве аргумента передаётся в метод_ ExecuteCommand_\. Соответствующий параметр — _rawCommand —_ используется для создания SQL команды \(_sqlCommand_\), которая затем исполняется\. Выглядит небезопасно, но явного taint\-источника здесь нет\.

Предположим, что рассмотренный выше код — часть какой\-то библиотеки\. Назовём её SQLILib\.dll\. Эта библиотека используется в приложении \(пусть будет SQLIApp\.exe\), где метод _ProcessUserInfo_ используются следующим образом:

```cpp
static void TestHelper(DBHelper helper)
{
  var userName = Request.Form["userName"];
  helper.ProcessUserInfo(userName);
}
```

Здесь данные, полученные от пользователя \(_Request\.Form\["userName"\]_\), напрямую передаются в метод _ProcessUserInfo_\. Так как _ProcessUserInfo_ — метод из внешней библиотеки, так просто его код не посмотреть\.

В итоге выходит, что данные были получены напрямую от пользователя и переданы в метод, где они также без проверки используются — выглядит опасно\. 

Да, можно предположить, что между пользователями библиотеки и её разработчиками есть некая договорённость о том, что в методы должны приходить уже проверенные данные\. Например, это указано в документации к API\. Но, когда речь идёт про безопасность, лучше перестраховаться\.

К сожалению, при анализе кода SQLIApp\.exe анализатор не сможет предупредить о возможной SQL инъекции, так как ничего не знает про метод _ProcessUserInfo_\. С другой стороны, анализатор может выдать предупреждение при анализе кода самой библиотеки\.

Выдавать ли предупреждения, когда источником заражённых данных считаются параметры — решать каждой диагностике, использующей taint\-информацию\. Для диагностики по поиску SQL инъекций мы решили выдавать предупреждения на низком уровне достоверности\.

**Примечание**\. Если вы не хотите видеть подобных предупреждений, то можете отключить их с помощью комментария вида _//\-V::5608:3_ в \.pvsconfig файле\. Тогда предупреждения V5608 \(SQLI\) низкого уровня достоверности не попадут в лог\. Более подробно про \.pvsconfig файлы можно почитать в документации "[Подавление ложных предупреждений](https://pvs-studio.ru/ru/docs/manual/0017/)" \(раздел "Подавление ложных предупреждений с помощью файлов конфигурации диагностик \(\.pvsconfig\)"\)\.

И наоборот, если вы считаете эти предупреждения крайне важными, то можете повысить их уровень достоверности до выского, используя _//V\_LEVEL\_1::5608_\. Подробности приводятся в главе "Как задать свой уровень для конкретной диагностики" в разделе документации "[Дополнительная настройка диагностик](https://pvs-studio.ru/ru/docs/manual/0040/)"\.

### Приёмники заражённых данных \(taint sinks\)

Приёмники заражённых данных являются уникальными для каждого диагностического правила\. Поэтому они скорее относятся к диагностикам, чем к общему механизму taint\-анализа\. Как мы обсуждали, суть приёмников в том, что в них не должны попадать заражённые данные\. Если в приложении есть путь, когда данные из taint source попадают в taint sink — беда\.

Например, в случае с SQL инъекциями, приёмником может служить конструктор класса _SQLCommand_ или метод _FromSqlRaw_\.

Пример:

```cpp
var taintedStr = GetTaintedData();
var sqlCommand = new SqlCommand(taintedStr); // taint sink
....
```

Можно было бы подумать, что конструктор класса _SqlCommand_ — это, скорее, передатчик, а приёмником выступает какой\-нибудь из методов исполнения команды: _SqlCommand\.ExecuteSomehow_\. Однако кажется странным сначала создать заражённую команду, а потом пытаться её проверять\. Проще и более очевидно проверить данные, на основе которых команда создаётся\. Именно поэтому в нашем случае конструктор _SqlCommand_  — sink, а не передатчик данных\.

Таким же приёмником является свойство _SqlCommand\.CommandText_\. Пример опасного кода:

```cpp
void ProcessUserInfo()
{
  using (SqlConnection connection = new SqlConnection(_connectionString))
  {
    ....
    String userName = Request.Form["userName"];

    using (var command = new SqlCommand()
    {
      Connection = connection,
      CommandText = "SELECT * FROM Users WHERE UserName = '" + userName + "'",
      CommandType = System.Data.CommandType.Text
    })
    {            
      using (var reader = command.ExecuteReader())
        ....
    }
  } 
}
```

Здесь создаётся экземпляр типа _SqlCommand_, однако заражённая строка не передаётся в качестве аргумента в конструктор, а используется для инициализации свойства _CommandText_\.

### Валидация заражённых данных

Естественно, что не все цепочки заражённых данных ведут от источников к приёмникам\. Есть несколько причин прерывания:

* анализатор не может отследить дальнейшее распространение данных, из\-за этого цепочка разрывается;
* данные проходят верификацию, после чего больше не считаются заражёнными;
* данные проходят преобразование, в результате которого сконвертированные данные уже не имеют статус заражённых\.

Однако следует помнить, что, если валидация происходит только при определённых условиях, это может быть даже более опасным случаем из\-за иллюзии безопасности\.

Способы валидации будут различаться для разных типов данных\. Зависит от того, что мы ожидаем на вход: данные для SQL команды, какой\-нибудь путь и т\.п\. Для защиты от SQLI, например, повсеместно рекомендуют использование параметризированных запросов\.

```cpp
String userName = Request.Form["userName"];    
using (var command = new SqlCommand()
{
  Connection = connection,
  CommandText = "SELECT * FROM Users WHERE UserName = @userName",
  CommandType = System.Data.CommandType.Text
})
{
  var userNameParam = new SqlParameter("@userName", userName);
  command.Parameters.Add(userNameParam);
            
  using (var reader = command.ExecuteReader())
    ....
}
```

В данном случае цепочка передачи taint\-данных просто разорвётся при создании объекта типа _SqlParameter_\. У анализатора нет информации о том, что он передаёт заражение или заражается\. Как следствие, анализатор не будет считать переменную _userNameParam_ заражённой\. Исходное значение _userName_ в создаваемую команду никак не попадает, поэтому предупреждение выдано не будет\.

### Распространение заражённых данных

Естественно, что данные не попадают сразу из taint source в taint sink\. Теоретически могут, конечно, но это уже совсем фантастический код\. :\) Скорее всего, после проникновения в приложение заражённые данные сначала распространяются различными путями и лишь затем могут попасть в taint sink\. Причём способы заражения могут быть достаточно вариативными\. Простые присвоения переменных — это самый очевидный кейс\. 

Соответствующий пример, на самом деле, уже был продемонстрирован ранее:

```cpp
void Example()
{
  var taintedVariable = Console.ReadLine();
  TaintSink(taintedVariable);
}
```

В этом фрагменте кода статус заражённого значения проставляется на вызов метода _Console\.ReadLine\(\)_ и лишь затем передаётся переменной _taintedVariable_ через операцию присвоения\.

Заражения через переприсвоения аналогичны:

```cpp
var taintedVariable = Console.ReadLine();
var taintedVariable2 = taintedVariable;
```

Дополнительно мы рассмотрели ряд более интересных кейсов передачи заражения\. Например, в строках заражение вполне себе передаётся через конкатенацию:

```cpp
var shipCity = Console.ReadLine();
var resStr 
  = "select * from OrdersTable where ShipCity = '" + shipCity + "'";
```

Анализируя операции конкатенации строк, мы проверяем, не является ли один из операндов заражённым\. Если это так, выставляем соответствующий статус для всего выражения\.

С таким же успехом данные могут передаваться через интерполированные строки:

```cpp
var resStr = $"select * from UsersTable where Id = '{id}'";
```

Принцип аналогичен — мы анализируем интерполируемые элементы и, если хотя бы один из них заражён, выставляем статус заражения на всё выражение\.

Ещё один способ передачи заражения — через вызов методов\. Здесь тоже есть, где разгуляться\. :\)

Один из вариантов — транслирование заражения из аргументов в возвращаемое значение\. Например:

```cpp
var resStr = String.Join(separator, nonTaintedStr, taintedStr);
```

В результате выполнения этого кода при соединении строк заражение передастся от значения _taintedStr_ в возвращаемое значение метода _String\.Join_, а оттуда — в _resStr_\.

Другой интересный кейс, с которым довелось столкнуться, — заражение объектов через аргументы метода, вызываемого у данного объекта\. Типичный представитель подобных случаев — _StringBuilder_\.

```cpp
var sb = new StringBuilder();
sb.AppendLine(taintedStr);
var resStr = sb.ToString();
```

Изначально _sb_ не является заражённым, но становится таковым в результате вызова метода _AppendLine_, если _taintedStr_ содержит заражённые данные\. Теперь, когда объект _sb_ является инфицированным, он может сам передавать заражение дальше\. В нашем случае заражается переменная _resStr_ в результате вызова метода _ToString_, транслирующего статус заражённости из вызывающего объекта в возвращаемое значение\.

Естественно, все эти способы могут комбинироваться, а заражение затем может уйти вообще в другой метод — и такие случаи тоже неплохо бы обнаруживать\.

### Что не получилось?

Основное и самое обидное, с чем довелось столкнуться, — ограничения анализа значимых типов\. Дело в том, что сейчас data\-flow анализ в C\# ограничен перечислениями и целочисленными типами, такими как _int_, _byte_, _short_ и т\.п\. Из\-за этого, как только в цепочке заражений встречается неизвестный значимый тип \(какая\-нибудь структура, например\), она обрывается\. Это большая точка для роста и доработок\.

Вообще, так как это самая первая версия taint\-анализа, уже есть некоторые идеи по доработкам и улучшениям\. Будем допиливать со временем, в частности по мере разработки новых диагностических правил\. Если столкнётесь с false positive срабатываниями или, наоборот, анализатор что\-то не обнаружит, пожалуйста, [напишите нам](https://pvs-studio.ru/ru/about-feedback/)\. Мы постараемся учесть эти случаи и поддержать их в будущем\.

### Работа taint\-based диагностических правил

От общих механизмов переходим к их использованию\. В принципе, здесь всё достаточно просто и общий алгоритм работы примерно одинаков\.

1. При обходе дерева нужно обрабатывать те синтаксические конструкции, которые могут быть sink'ом данной диагностики\.
1. При посещении соответствующей конструкции — проверять, является ли она sink'ом\.
1. Если это наш sink, проверять, что приходящие в него данные имеют taint\-статус и не имеют соответствующей метки валидации\.
1. \.\.\.
1. PROFIT\!

Конечно, в диагностиках будет дополнительная логика, но общий принцип останется, думаю, примерно одинаковым\.

## Первая диагностика на taint\-анализе: поиск возможных SQLI

Как я упоминал выше, первой диагностикой на taint\-анализе стало правило поиска потенциальных SQL инъекций\.

Не буду вдаваться в подробности, что такое SQL инъекция, какие виды бывают и т\.д\. В интернете достаточно информации на эту тему\. Например, вот ссылочки на [Википедию](https://en.wikipedia.org/wiki/SQL_injection) и на [docs\.microsoft\.com](https://docs.microsoft.com/en-us/sql/relational-databases/security/sql-injection?view=sql-server-ver15)\. Обозначу только суть\.

А суть очень проста и отлично ложится на базовую теорию taint\-анализа, про которую мы рассуждали выше\. Есть какой\-то источник внешних данных \(taint source\)\. Данные в этом источнике поступают от пользователя, и он волен писать в них то, что ему вздумается\. Эти данные приходят в приложение, перемещаются по нему и в результате попадают в SQL команду без проверки \(sink\)\. Если команда создаётся таким образом, что допустимыми являются любые данные от пользователя, он может скомпрометировать их так, чтобы выполнить произвольный запрос\. Вот вам и инъекция\.

![0831_TaintAnalysis_Cs_ru/image3.png](https://import.viva64.com/docx/blog/0831_TaintAnalysis_Cs_ru/image3.png)

Рассмотрим пример\. Если вы уже гуглили запросы в духе "SQLI C\#", то наверняка видели что\-нибудь подобное:

```cpp
private HttpRequest Request { get; set; }
void ProcessUserInfo()
{
  using (SqlConnection connection = new SqlConnection(_connectionString))
  {
    ....
    String userName = Request.Form["userName"];

    using (var command = new SqlCommand()
    {
      Connection = connection,
      CommandText = "SELECT * FROM Users WHERE UserName = '" + userName + "'",
      CommandType = System.Data.CommandType.Text
    })
    {            
      using (var reader = command.ExecuteReader())
        ....
    }
  } 
}
```

Проблема в том, что данные в _userName_ поступают извне и подставляются в SQL команду без каких\-либо проверок\. Это чревато последствиями, если вместо адекватных данных в _userName_ попадёт скомпрометированная команда\. Например, если там будет строка _' OR '1'\='1_, то обработка данных запустится для всех элементов таблицы, а не одного пользователя\.

Теперь разберём этот пример с точки зрения анализатора\. Как он может обнаружить здесь угрозу возникновения SQLI?

В данном случае источником заражённых данных выступает свойство _Request\.Form_ \(переменная _Request_ имеет тип _HttpRequest_\)\. Тип свойства _Form_ — _NameValueCollection_, и именно экземпляр этого типа мы считаем заражённым\. У объекта вызывается индексатор, который ретранслирует заражённые данные на всё выражение \(возвращаемое значение индексатора\): _Request\.Form_ \-\> _Request\.Form\["userName"\]_\. Так как инициализатор переменной _userName_ заражён, она также становится заражённой\.

Анализируем вызов конструктора типа _SqlCommand_, точнее инициализацию свойств\. Нас интересует свойство _CommandText_\. В нашем случае _CommandText_ — sink\. То есть при записи в него заражённых данных должна срабатывать диагностика\. Поэтому анализируем правую часть присваивания: _"SELECT \* FROM Users WHERE UserName \= '" \+ userName \+ "'"_\. Ага, конкатенация строк\. Мы помним, что, если хотя бы один операнд имеет заражение, оно передаётся на всё выражение\. Как мы помним, _userName_ заражён\. Так что в результате конкатенации становится заражённым и всё выражение целиком\. Выходит, значение, записываемое в _CommandText_, заражено, а это как раз то, что мы хотели проверить\.

## Заключение

Taint анализ в описанном виде появился в релизе PVS\-Studio 7\.13\. Там есть и диагностика по поиску возможных SQLI — [V5608](https://pvs-studio.ru/ru/docs/warnings/v5608/)\. Вы уже можете попробовать [новую версию анализатора](https://pvs-studio.ru/ru/pvs-studio/download/)\.

Безусловно, работы в этом направлении много\. И как по улучшению общего механизма taint\-анализа, так и по разработке диагностик, его использующих\. Поэтому, если у вас есть какие\-нибудь идеи по улучшению, смело [пишите нам](https://pvs-studio.ru/ru/about-feedback/), возьмём себе на заметку\.

Как обычно, приглашаю подписываться на мой [Twitter\-аккаунт](https://twitter.com/_SergVasiliev_)\. ;\)