﻿# Taint\-анализ в C и C\+\+ анализаторе PVS\-Studio

Ваш код принимает данные извне? Поздравляем, вы вступили на минное поле\! Любой непроверенный ввод от пользователя может привести к уязвимости, и найти все "растяжки" вручную в большом проекте почти невозможно\. Но есть "сапёр" — статический анализатор\. Инструмент нашего "сапёра" — taint\-анализ \(aka анализ помеченных данных\)\. Он позволяет обнаружить "грязные" данные, дошедшие до опасных мест без проверки\. Сегодня мы расскажем о том, как он работает\.

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

## Да кто такой этот ваш taint\-анализ?\!

Вспомните свою разработческую молодость\. Наверняка сразу после "Hello, World" вы могли написать что\-то вроде такого:

```cpp
int main()
{
  char name[256];
  printf("Stand up. There you go. "
         "You were dreaming. What's your name?\n");
  scanf("%255s", name);
  printf(name);

  return 0;
}
```

Давайте мы пока закроем глаза на возможное переполнение буфера `name` при пользовательском вводе — это всего лишь наша вторая программа\. Будем честны, этого количества символов хватит всем\. Ну\.\.\. [почти всем](https://www.guinnessworldrecords.com/news/2025/10/meet-the-man-with-the-worlds-longest-name-who-had-to-win-a-legal-battle-to-keep-it)\.  :\) Однако в этой программе мы также сталкиваемся и с другой проблемой: в строке `name` содержатся _помеченные данные_\. К чему может привести передача такой строки в функцию `printf`, мы рассказывали в [отдельной статье](https://pvs-studio.ru/ru/blog/posts/cpp/0129/)\.

"Такие косяки допускают только студенты", — скажете вы\. Спешу раздосадовать: мы находили подобные [ошибки](https://pvs-studio.ru/ru/blog/posts/cpp/0599/) вовсе не в курсовых работах\.

Чтобы находить их на этапе написания кода, можно использовать статические анализаторы с применением технологии [taint\-анализа](https://pvs-studio.ru/ru/blog/terms/6496/)\. Давайте же заглянем под капот C и C\+\+ анализатора PVS\-Studio и подробнее рассмотрим его устройство\.

## Да, у нас была стратегия

Одним из механизмов, применяемых компиляторами для оптимизаций, а статическими анализаторами для нахождения ошибок и уязвимостей в коде, является [анализ потока данных](https://pvs-studio.ru/ru/blog/terms/7004/)\.

Для обнаружения таких ошибок, как переполнение, деление на ноль или выход за пределы массива анализатор должен обладать информацией о значении переменной/операнда в потенциально ошибочном выражении\. В зависимости от пути выполнения программы переменная может иметь разные значения\. Виртуальное значение переменной — это множество всех значений, которые она может иметь в конкретной точке выполнения программы\. Рассмотрим пример:

```cpp
int getElement(int index)
{
  int arr[2] {0, 1};
  return arr[index];
}

int main()
{
  int index = 0;                         // index = 0
  std::cin >> index;                     // index = [INT_MIN; INT_MAX]
  index &= 1;                            // index = [0; 1]
  int element = getElement(index);
  return element;
}
```

В примере отмечено, какое виртуальное значение имеет переменная `index` в каждой точке выполнения программы\.

1. При объявлении и инициализации переменой присваивается значение `0`;
1. При получении индекса из консоли его значение становится любым и ограничивается только фантазией пользователя и диапазоном типа, т\.е\. `[INT_MIN; INT_MAX]`;
1. После операции побитового "И" значение `index` принадлежит диапазону `[0; 1]`\.

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

Но что делать, если нам неизвестно виртуальное значение переменной? Например, это параметр функции\. Давайте удалим функцию `main` из прошлого примера:

```cpp
int getElement(int index)
{
  int arr[2] {0, 1};
  return arr[index]; // index is unknown
}
```

Вот перед нами просто функция, возвращающая значение массива по индексу\. Во время анализа тела функции без межпроцедурной информации значение параметра `index` анализатору неизвестно\.

Может возникнуть закономерный вопрос: "Разве `index = [INT_MIN; INT_MAX]` и `index is unknown` не одно и то же?"

Нет, это не одно и то же\. Пожалуй, тут нужно отступить и пояснить один важный момент\.

Существуют _sound_ и _unsound_ стратегии статического анализа\. Обычно анализатор работает в unsound\-стратегии и генерирует предупреждения только если **может доказать наличие ошибки**\. И при такой стратегии предупреждение о выходе за границу массива в последнем примере выдано не будет\. Мы не знаем значения индекса и несмотря на то, что выход за границу массива возможен, утверждать о наличии ошибки нельзя\. В противном случае мы просто утонем в ложных и бессмысленных срабатываниях\.

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

Taint\-анализ стоит особняком посреди двух стратегий\. Несмотря на то, что в теории это часть sound стратегии, его применение ограничено жизненным циклом потенциально помеченных данных, лежащем в узком коридоре на пути из источника к стоку, а потому и побочные эффекты в виде ложных срабатываний здесь не так сильно выражены\.

Не будем глубоко вдаваться в описание теории taint\-анализа и статического анализа, в целом\. В статьях про Java анализатор мы уже [касались](https://pvs-studio.ru/ru/blog/posts/java/1214/) этой темы и даже [подробно её рассматривали](https://pvs-studio.ru/ru/blog/posts/java/1198/)\.

## Заглянем под капот

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


> \*\*Примечание\\\.\*\* Помимо общеизвестных источников потенциально помеченных данных, таких как \`std::cin\` и \`scanf\`, в PVS\\\-Studio есть возможность добавления пользовательских источников с помощью \[аннотаций в формате JSON\]\(https://pvs\-studio\.ru/ru/blog/posts/1166/\)\\\.

Например, так пользователь может добавить свой собственный источник потенциально помеченных данных, проаннотировав его как `taint_source`:

```cpp
std::string ReadStrFromStream(std::istream &input, std::string &str)
{
  ....
  input >> str; 
  return str;
  ....
}

"annotations": [
  ....
  {
    "type": "function",
    "name": "ReadStrFromStream",
    "params": [
      {
        "type": "std::istream &input"
      },
      {
        "type": "std::string &str",
        "attributes": [ "taint_source" ] // <=
      }
    ],
    "returns": { "attributes": [ "taint_source" ] }
  }
  ....
]
```

Подробнее про аннотации в формате JSON можно прочитать в [нашей документации](https://pvs-studio.ru/ru/docs/manual/6743/)\.

Полученные из источников данные считаются помеченными и отмечаются соответствующим тегом\. При операциях с такими данными их статус может распространяться \(taint propagation\)\.

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

```cpp
int taintedData;
scanf("%d", &taintedData);

int res = 5 + taintedData; // res now is tainted data too
....

// The same thing happens when using 
// tainted data in a loop condition
int taintedData;
scanf("%d", &taintedData);

for (int i = 0; i < taintedData; i++)
{
  //The value of i is in range [0; taintedData]
  //as a top bound of the range is tainted
  //the variable derived taint status too
}
```

Идём дальше и подходим к концу жизненного цикла помеченных данных — к приёмникам\.

Приёмники потенциально помеченных данных \(taint sink, сток\) — это функции или операции, чувствительные к достоверности аргументов/операндов\. Такими стоками, например, являются функции, выделяющие память, операция деления на ноль и оператор индексирования\.

```cpp
template <typename T, size_t size>
const auto &getElement(const std::array<T, size> &arr, size_t index)
{
  return arr[index]; // taint-sink operation
}

int main()
{
  size_t index;
  scanf("%zu", &index);

  std::array<int, 5> arr { 1, 2, 3, 4, 5 };
  return getElement(arr, index);
}
```

А с помощью аннотации `taint_sink` можно определить пользовательскую функцию\-сток:

```cpp
{
  ....
  "annotations": [
    {
      {
      "type": "function",
      "name": "DoSomethingWithData",
      "params": [ { "type": "std::string &str",
                    "attributes": [ "taint_sink" ] }] // <=
      }
    }
  ]
}
```

## Казалось бы, при чём здесь контракты?

В С\+\+26 в язык будут добавлены [контракты](https://en.cppreference.com/w/cpp/language/contracts.html)\. Да, они предназначены для обнаружения ошибок на этапе выполнения, но и статическому анализу они будут весьма полезны\. Дело в том, что очень многие функции уже сейчас обладают неявными контрактами\. Например, операция деления предполагает, что делитель не может быть равен нулю\.

С помощью нового синтаксиса можно явно определить контракт функции \([Function contract specifiers](https://en.cppreference.com/w/cpp/language/functions/function_contract_specifiers.html#Precondition)\)\. Такие условия описываются в объявлении функции и доступны даже без её тела, что даёт возможность статическому анализатору сопоставлять предикаты с виртуальными значениями аргументов и выявлять ошибки\.

Функция\-сток, по сути, обладает контрактом, накладывая его на аргументы\. Например, так будет выглядеть функция `getElement` с добавлением контрактов:

```cpp
template <typename T, size_t size>
const auto &getElement(const std::array<T, size> &arr, size_t index)
  pre(index < std::size(arr))
{
  return arr[index];
}
```

Однако интересно то, что аналогичный неявный контракт имеет и сам `operator[]`, т\.е\. при доступе к массиву индекс проверяется на соответствие контракту, и в случае его нарушения анализатор выдаёт предупреждение\. Подобным образом анализатор может определять неявные контракты функций во время межпроцедурного анализа, основываясь на их содержании\. Например, если аргументы функции внутри её тела передаются в функцию\-сток, то контракт этого стока будет применён к функции\-обёртке, и у диагностических правила появится больше информации для работы\.

Разобравшись с условиями, при которых анализатор генерирует предупреждения при взаимодействии со стоками, перейдём к вопросу верификации данных\. Очевидно, что для исправления ошибок при использовании помеченных данных их нужно каким\-то способом проверить\. Но как определить, что данные прошли верификацию и перестали быть потенциально помеченными? В случае с пользовательскими аннотациями это элементарно: данные, прошедшие через функцию, определённую пользователем как санитайзер, считаются достоверными\. Но что, если пользовательских аннотаций нет? Неужели только и остается, что ориентироваться на "validate" и "check" в именах функций?

## Верификация

Как мы определили выше, помеченными считаются данные, полученные извне и не прошедшие достаточной проверки\.

И тут можно задаться вопросом: "А что значит "достаточная проверка"?"

Это хороший вопрос\. Верификация является самой интересной частью taint\-анализа\. Именно этот этап описывают правила проверки помеченных данных\.

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

* регулярные выражения\. Строки можно проверять, сопоставляя с паттернами регулярных выражений;
* экранирование\. Замена управляющих последовательностей в потенциально помеченном тексте предотвращает выполнение вредоносного кода\. Такой метод применяется при работе с SQL\-инъекциями;
* приведение типов\. Способ проверки, применяемый к числовым данным\. Включает в себя проверку диапазона допустимых значений, точности значений;
* белый список\. Сопоставление введённых данных с белым списком допустимых вариантов значений\.

Но как удостовериться, что проверка будет исчерпывающей? Рассмотрим пример:

```cpp
int getElement(int array[5], int index)
{
  scanf("%d", &index);

  if (index >= 0)
  {
    return array[index];
  }
  
  return 0;
}
```

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

Предупреждение PVS\-Studio: V557 Array underrun/overrun is possible\. The 'index' index is from potentially tainted source\.

Попробуем выполнить более точную проверку:

```cpp
int getElement(int array[5], int index)
{
  scanf("%d", &index);

  if (index >= 0 && index < 6)
  {
    return array[index];
  }
  
  return 0;
}
```

На этот раз мы проверили обе границы диапазона индекса, введённого пользователем, но проверили неправильно, поэтому использование его в `operator[]` всё равно может привести к выходу за границу массива\. Это все ещё ошибка, вызванная помеченными данными? Или уже нет?

Предупреждение PVS\-Studio: V557 Array overrun is possible\. The value of 'index' index could reach 5\.

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


> Даже внутри нашей команды мнения насчёт проверки taint\\\-данных разделились\\\. Некоторые считают, что проверки двух границ для целочисленных данных достаточно, и в этом примере выход за границу массива уже никак с потенциально помеченными данными не связан\\\. Однако есть и противоположное мнение: несмотря на проверку, пользовательский ввод все ещё может спровоцировать ошибку, а значит, и предупреждение о потенциально помеченных данных здесь уместно\\\.
> 
> 
> 
> А что вы думаете об этом примере? Когда помеченные данные перестают быть таковыми?

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

Помимо ориентированности taint\-анализа на поиск потенциальных уязвимостей, связанных с высокоуровневым кодом \(SQL\-инъекции, XXE, XEE и т\.д\.\), существует также необходимость применять его и на гораздо более низком уровне\. Например, в операциях индексирования, деления на ноль, выделении памяти и т\.д\. Поэтому taint\-анализ востребован даже в условиях отсутствия пользовательских аннотаций\.

В такой ситуации можно рассчитывать на оставшиеся два метода верификации: приведение типов и сопоставление с "белым" списком допустимых значений\. Например, можно считать достоверными переменные, проверенные функциями `equal` или `contains`\.

Как мы уже успели выяснить, проверку обеих границ диапазона для знаковых чисел и верхней границы для беззнаковых PVS\-Studio считает верификацией\. Например, внутри блока `if` при таком условии мы получим достоверный диапазон значений переменной:

```cpp
int array[16] { };
int index;
scanf("%d", &index);

if (index > 0 && index < 16)
{
  // index = [1; 15] inside the if statement
  return array[index];
}
```

Некоторые арифметические операции также могут выполнять верификацию числовых данных\. Например, в результате деления числа по модулю мы точно можем посчитать результирующий диапазон, а значит, данные уже не являются помеченными\. Таким же образом можно интерпретировать результат побитового "И"\.

```cpp
int array[16] { };
int index;
scanf("%d", &index);

int trustedValue1 = index & 0xf; // trustedValue1 = [0; 15]
array[trustedValue1] = 0;

int trustedValue2 = index % 16;  // trustedValue2 = [0; 15]
array[trustedValue2] = 0;
```

Пока PVS\-Studio не поддерживает аннотаций для функций\-санитайзеров, однако в ближайшее время планируется реализовать этот функционал\.

## Ну и в заключение

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

Для увеличения эффективности пользователь может применять [аннотации в формате JSON](https://pvs-studio.ru/ru/docs/manual/6810/), добавляя свои собственные источники и стоки помеченных данных\. В любом случае, PVS\-Studio может предложить базовый taint\-анализ даже в отсутствие пользовательских аннотаций\. А с приходом контрактов в C\+\+26 качество анализа вырастет ещё сильнее\.

Если вы хотите попробовать анализ помеченных данных самостоятельно, приглашаем вас воспользоваться [бесплатной 30\-дневной версией](https://pvs-studio.ru/ru/pvs-studio/try-free/) анализатора PVS\-Studio\.