﻿# Под капотом SAST: как инструменты анализа кода ищут дефекты безопасности

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

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


> Статья написана на основе доклада "\[Под капотом SAST: как инструменты анализа кода ищут дефекты безопасности\]\(https://youtu\.be/cREwHy4H3jM\)" с TechLead Conf 2022\\\. Содержимое адаптировано для читаемости: что\\\-то сокращено, что\\\-то модифицировано\\\.



SAST \(Static Application Security Testing\) — подход к поиску дефектов безопасности без исполнения приложения\. Если "классический" статический анализ — про поиск ошибок, то SAST — про поиск потенциальных уязвимостей\.

Как мы видим SAST снаружи? Берём исходники, отдаём их анализатору, а на выходе получаем отчёт со списком возможных проблем безопасности\. 

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

Основная цель статьи — ответить на вопрос, как SAST\-инструменты ищут потенциальные уязвимости\.

## Типы используемой информации

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

### Синтаксическая информация

Для работы анализаторы используют промежуточное представление кода\. Самые распространённые — синтаксические деревья \(абстрактное синтаксическое дерево или дерево разбора\)\. 

Рассмотрим паттерн ошибки:

```cpp
operand#1 <operator> operand#1
```

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

```cpp
a == a
```

Однако приведённый выше случай — частный, вариаций — множество:

* один или оба операнда могут быть обёрнуты в скобки;
* оператором может быть не только '\=\=', но и '\!\=', '\|\|' и т\. п\.
* операндами могут быть не идентификаторы, а обращения к элементам, вызовы функций и т\. п\.

Анализировать код как простой текст в таком случае неудобно\. Здесь и выручают синтаксические деревья\. 

Рассмотрим выражение _a \=\= \(a\)_\. Дерево разбора для него может выглядеть так:

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

Работать с такими деревьями удобно: есть информация о структуре, извлекать из выражений операнды и операторы просто\. Нужно опустить скобки? Тоже не проблема, просто спускаемся по дереву\. 

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

### Семантическая информация

Рассмотрим пример:

```cpp
if (lhsVar == rhsVar)
{ .... }
```

Если _lhsVar_ и _rhsVar_ — переменные типа _double_, с этим кодом могут возникнуть проблемы\. Например, если и _lhsVar_ и _rhsVar_ точно равны 0\.5, это сравнение даст _true_\. Однако если одно значение будет равно 0\.5, а второе — 0\.4999999999999, то проверка уже даст _false_\.  Здесь встаёт вопрос: какого поведения ожидает разработчик? Если он рассчитывает, что подобная разница находится в пределах допустимой погрешности, сравнение нужно переписать\.

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

Представим: анализатор проверяет код и встречает такое выражение:

```cpp
if (lhsVar == rhsVar)
{ .... }
```

Вопрос: нужно здесь ругаться или не нужно? Можно посмотреть дерево, понять, что операнды — идентификаторы, что инфиксная операция — сравнение\. Однако мы не можем сказать, опасный этот кейс или нет, т\. к\. не знаем типов переменных _lhsVar_ и _rhsVar_\. 

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

* какой тип \(в терминах языка программирования\) имеет соответствующее узлу выражение;
* какой сущностью представлен узел: локальная переменная, параметр, поле и т\. п\.;
* \.\.\.

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

![1028_SAST_Under_The_Hood_ru/image4.png](https://import.viva64.com/docx/blog/1028_SAST_Under_The_Hood_ru/image4.png)

### Аннотации функций

Синтаксиса и семантики порой бывает недостаточно\. Рассмотрим пример:

```cpp
IEnumerable<int> seq = null;
var list = Enumerable.ToList(seq);
....
```

Метод _ToList_ объявлен во внешней библиотеке, доступа к исходникам у анализатора нет\. Есть переменная _seq_ со значением _null_, которая передаётся в упомянутый _ToList_\. Это безопасная операция или нет?

Воспользуемся синтаксической информацией\. Можно понять, где здесь литерал, где идентификатор, а где — вызов метода\. А вызов метода безопасный? Непонятно\. 

Попробуем семантику\. Можно понять, что _seq_ — локальная переменная, а по\-хорошему даже посчитать её значение\. Что можно узнать о _Enumerable\.ToList_? Например, тип возвращаемого значения и тип параметра\. А _null_ внутрь безопасно передавать? Непонятно\. 

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

Условная аннотация для метода _ToList_ в коде анализатора может выглядеть так:

```cpp
Annotation("System.Collections.Generic",
           nameof(Enumerable),
           nameof(Enumerable.ToList),
           AddReturn(ReturnFlags.NotNull), 
           AddArg(ArgFlags.NotNull));
```

Основная информация, которую несёт эта аннотация:

* полное имя метода \(включая имя типа и пространства имён\)\. При наличии перегрузок могут понадобиться доп\. сведения о параметрах;
* ограничения на возвращаемое значение\. _ReturnFlags\.NotNull_ сообщает, что возвращаемое значение не будет равно _null_;
* ограничения на входные значения\. _ArgFlags\.NotNull_ говорит анализатору, что единственный аргумент метода не должен иметь значения _null_\.

Вернёмся к изначальному примеру:

```cpp
IEnumerable<int> seq = null;
var list = Enumerable.ToList(seq);
....
```

С наличием механизма аннотаций анализатор знает ограничения метода _ToList_\. Если он отследит значение переменной _seq_, то сможет выдать предупреждение о возникновении исключения типа _NullReferenceException_\. 

## Разновидности анализа

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

### Pattern\-based analysis

Иногда "обыкновенные" ошибки на самом деле являются дефектами безопасности\. Рассмотрим пример такой уязвимости\.

**iOS: CVE\-2014\-1266**

Информация об уязвимости:

* CVE\-ID: [CVE\-2014\-1266](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-1266)
* CWE\-ID: [CWE\-20: Improper Input Validation](https://cwe.mitre.org/data/definitions/20.html)
* [Запись в базе NVD](https://nvd.nist.gov/vuln/detail/CVE-2014-1266)
* Описание: _The SSLVerifySignedServerKeyExchange function in libsecurity\_ssl/lib/sslKeyExchange\.c in the Secure Transport feature in the Data Security component in Apple iOS 6\.x before 6\.1\.6 and 7\.x before 7\.0\.6, Apple TV 6\.x before 6\.0\.2, and Apple OS X 10\.9\.x before 10\.9\.2 does not check the signature in a TLS Server Key Exchange message, which allows man\-in\-the\-middle attackers to spoof SSL servers by \(1\) using an arbitrary private key for the signing step or \(2\) omitting the signing step\._

Код:

```cpp
....
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
  goto fail;
  goto fail;
if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0)
  goto fail;
....
```

При беглом взгляде может показаться, что с кодом всё в порядке\. На самом деле второй _goto_ — безусловный\. Из\-за этого проверка с вызовом метода _SSLHashSHA1\.final_ никогда не выполнялась\.

По\-хорошему, код должен быть отформатирован так:

```cpp
....
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
  goto fail;
goto fail;
if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0)
  goto fail;
....
```

Как поймать подобный дефект статическим анализом? 

Первый способ — посмотреть, что _goto_ — безусловный, а за ним есть выражения без каких\-либо меток\. 

Возьмём упрощённый код с тем же смыслом:

```cpp
{
  if (condition)
    goto fail;
    goto fail;
  ....
}
```

Дерево для него может выглядеть так:

![1028_SAST_Under_The_Hood_ru/image5.png](https://import.viva64.com/docx/blog/1028_SAST_Under_The_Hood_ru/image5.png)

_Block_ — набор высказываний\. Из дерева видно, что:

* один оператор _goto_ относится к оператору _if_, а второй — непосредственно к блоку;
* между _GotoStatement_ \(оператор перехода\) и _LabeledStatement_ \(метка перехода\) находится высказывание _ExpressionStatement_;
* _goto_, относящийся к блоку, выполняется безусловно, а метки перед _ExpressionStatement_ нет\. Значит, _ExpressionStatement_ в данном случае недостижим\.

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

Другой способ поймать дефект — посмотреть, что форматирование кода не соответствует логике исполнения\. 

Упрощённый алгоритм будет таким:

1. Посмотреть, сколько отступов перед _then_\-ветвью оператора _if_\. 
1. Взять следующее после _if_ высказывание\. 
1. Если высказывание находится на следующей после _then_\-ветви строке, при этом у них одинаковый отступ — выдать предупреждение\.  

![1028_SAST_Under_The_Hood_ru/image6.png](https://import.viva64.com/docx/blog/1028_SAST_Under_The_Hood_ru/image6.png)

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

### Data flow analysis

Рассмотрим пример:

```cpp
if (ptr || ptr->foo())
{ .... }
```

Разработчик накосячил с логикой и перепутал операторы '&&' и '\|\|'\. Получается, если _ptr_ — нулевой указатель, он будет разыменован\.

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

```cpp
if (ptr)
{ .... }
// 50 lines of code
....
auto test = ptr->foo();
```

Здесь указатель _ptr_ также проверяется на равенство _NULL_, а после разыменовывается без проверки — выглядит подозрительно\.

**Примечание**\. В тексте я использую _NULL_ для обозначения значения нулевого указателя, а не как макрос языка Си\.

Паттернами такой случай поймать будет сложновато\. Например, в коде выше нужно ругаться, а в коде ниже — нет, ведь _ptr_ на момент разыменования точно не будет нулевым указателем:

```cpp
if (ptr)
{ .... }
// 50 lines of code
....
if (ptr)
{
  auto test = ptr->foo();
  ....
}
```

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

Анализ потока данных \(data flow analysis\) помогает отслеживать значения выражений в разных точках кода\. На основе этих данных анализатор выдаёт предупреждения\.

Data flow анализ применим к разным типам данных\. Примеры:

* boolean: _true_ или _false_;
* integer: диапазоны значений;
* pointers / references: null state\.  

Рассмотрим ещё раз пример с указателями\. Разыменование нулевого указателя — это дефект безопасности [CWE\-476: NULL Pointer Dereference](https://cwe.mitre.org/data/definitions/476.html)\.

```cpp
if (ptr)
{ .... }
// 50 lines of code
....
auto test = ptr->foo();
```

Первым делом анализатор встречает проверку _ptr_ на _NULL\._ Она накладывает ограничения на значение _ptr_: в _then_\-ветви оператора _if_ _ptr_ — не нулевой указатель\. Зная это, анализатор не выдаст предупреждения на подобный код:

```cpp
if (ptr)
{ 
  ptr->foo();
}
```

А какое значение имеет _ptr_ вне _if_?

```cpp
if (ptr)
{ .... }
// ptr - ???

// 50 lines of code
....
auto test = ptr->foo();
```

В общем случае — неизвестно\. Однако анализатор может учесть, что _ptr_ уже проверялся на _NULL\._ Разработчик тем самым объявляет контракт, что _ptr_ может иметь значение _NULL\._ Этот факт можно сохранить\.

В итоге, когда анализатор встретит выражение _auto test \= ptr\-\>foo\(\)_, он может проверить условия:

* точное значение _ptr_ на момент разыменования неизвестно;
* выше по коду _ptr_ проверялся на _NULL_\. 

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

Теперь посмотрим, как анализ потока данных работает с целочисленными типами\. Для этого возьмём код, в котором есть дефект безопасности [CWE\-570: Expression is Always False](https://cwe.mitre.org/data/definitions/570.html)\. 

```cpp
void DataFlowTest(int x) 
{ 
  if (x > 10) 
  {
    var y = x - 10;
    if (y < 0)
      ....
    if (y <= 1)
      ....
  }
}
```

Начнём по порядку\. Посмотрим на определение метода:

```cpp
void DataFlowTest(int x) 
{ .... }
```

В локальном контексте \(анализ внутри одного метода\) у анализатора нет информации о том, какое значение может иметь _x_\. Однако известен тип параметра — _int_\. Это уже позволяет ограничить диапазон возможных значений: \[\-2 147 483 648; 2 147 483 647\] \(при условии, что считаем _int_ размером 4 байта\)\. 

Дальше в коде встречается условие:

```cpp
if (x > 10)
{ .... }
```

Если анализатор заходит в _then_\-ветвь оператора _if_, это накладывает дополнительные ограничения на диапазон\. В _then_\-ветви значение _x_ находится в диапазоне \[11; 2 147 483 647\]\. 

Дальше идёт объявление и инициализация переменной _y_:

```cpp
var y = x - 10;
```

Так как анализатор знает ограничения значений _x_, он может вычислить и возможные значения _y_\. Для этого из граничных значений вычитается 10\. Получается, значение _y_ лежит в диапазоне \[1; 2 147 483 637\]\.

Дальше — оператор _if_:

```cpp
if (y < 0)
  ....
```

Анализатор знает, что в этой точке исполнения значение переменной _y_ лежит в диапазоне \[1; 2 147 483 637\]\. Получается, что _y_ всегда больше 0, а выражение _y < 0_ — всегда ложно\. 

Рассмотрим дефект безопасности, для поиска которого пригодится анализ потока данных\. 

**ytnef: CVE\-2017\-6298**

Информация об уязвимости:

* CVE\-ID: [CVE\-2017\-6298](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-6298)
* CWE\-ID: [CWE\-476 NULL Pointer Dereference](https://cwe.mitre.org/data/definitions/476.html)
* [Запись в базе NVD](https://nvd.nist.gov/vuln/detail/CVE-2017-6298)
* Описание: _An issue was discovered in ytnef before 1\.9\.1\. This is related to a patch described as "1 of 9\. Null Pointer Deref / calloc return value not checked\."_

Посмотрим на код:

```cpp
....
TNEF->subject.data = calloc(size, sizeof(BYTE));          
TNEF->subject.size = vl->size; 
memcpy(TNEF->subject.data, vl->data, vl->size);
....
```

Проанализируем, откуда здесь уязвимость:

1. Функция [_calloc_](https://en.cppreference.com/w/c/memory/calloc) выделяет блок памяти и инициализирует его нулями\. Если память выделить не удалось, _calloc_ возвращает нулевой указатель\.
1. Потенциально нулевой указатель записывается в поле _TNEF\-\>subject\.data_\.  
1. Поле _TNEF\-\>subject\.data_ используется как первый аргумент функции [_memcpy_](https://en.cppreference.com/w/c/string/byte/memcpy)\. Если первый аргумент _memcpy_ будет нулевым указателем, возникнет неопределенное поведение\. Как мы помним, _TNEF\-\>subject\.data_ может быть нулевым указателем\.

Чтобы найти такую проблему, пригодятся и аннотации, и анализ потока данных\. 

Аннотации:

* _calloc_ может вернуть нулевой указатель;
* первый аргумент _memcpy_ не должен быть нулевым указателем \(второй, кстати, тоже\)\. 

Анализ потока данных отслеживает:

* запись потенциально нулевого указателя из возвращаемого значения _calloc_ в _TNEF\-\>subject\.data_;
* перемещение значения в рамках поля _TNEF\-\>subject\.data;_
* попадание потенциально нулевого указателя в первый аргумент _memcpy_ из поля  _TNEF\-\>subject\.data_\.

![1028_SAST_Under_The_Hood_ru/image7.png](https://import.viva64.com/docx/blog/1028_SAST_Under_The_Hood_ru/image7.png)

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

### Taint analysis

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

Рассмотрим пример кода, уязвимого к [SQL\-инъекциям](https://pvs-studio.ru/ru/blog/terms/6507/):

```cpp
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())
    { /* Data processing */ }
  }
}
```

Здесь нас интересует вот что: 

* данные приходят от пользователя и записываются в переменную _userName_;
* _userName_ подставляется в запрос, который записывается в свойство _CommandText_;
* созданная SQL\-команда отдаётся на исполнение\.

Допустим, в качестве _userName_ от пользователя приходит строка _\_SergVasiliev\__\. Получившийся запрос будет выглядеть так:

```cpp
SELECT * FROM Users WHERE UserName = '_SergVasiliev_'
```

Исходная логика сохраняется — из базы извлекаются данные для пользователя с именем _\_SergVasiliev\__\. 

А теперь предположим, что от пользователя пришла такая строка: _' OR '1'\='1\._ После её подстановки в шаблон запрос будет выглядеть так:

```cpp
SELECT * FROM Users WHERE UserName = '' OR '1'='1'
```

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

Кстати, отсюда растут ноги мема про автомобили со странными номерами:

![1028_SAST_Under_The_Hood_ru/image8.png](https://import.viva64.com/docx/blog/1028_SAST_Under_The_Hood_ru/image8.png)

Посмотрим на уязвимый код ещё раз:

```cpp
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())
    { /* Data processing */ }
  }
}
```

Анализатор не знает точного значения, которое будет записано в _userName_\. Это может быть как безопасное _\_SergVasiliev\__, так и опасное _' OR '1'\='1_\. Сам код ограничений на строку тоже не накладывает\. 

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

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

Используют taint\-анализ как раз для поиска различного рода инъекций и тех дефектов безопасности, которые возникают из\-за недостаточной проверки пользовательского ввода\.

Для примера с SQL\-инъекцией taint\-анализ может построить такую трассу передачи данных, что поможет найти дефект безопасности:

![1028_SAST_Under_The_Hood_ru/image9.png](https://import.viva64.com/docx/blog/1028_SAST_Under_The_Hood_ru/image9.png)

Посмотрим на пример реальной уязвимости, для поиска которой может пригодиться taint\-анализ\.

**BlogEngine\.NET: CVE\-2018\-14485**

Информация об уязвимости:

* CVE\-ID: [CVE\-2018\-14485](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-14485)
* CWE\-ID: [CWE\-611 Improper Restriction of XML External Entity Reference](https://cwe.mitre.org/data/definitions/611.html)
* [Запись в базе NVD](https://nvd.nist.gov/vuln/detail/CVE-2018-14485)
* Описание: _BlogEngine\.NET 3\.3 allows XXE attacks via the POST body to metaweblog\.axd\._

Уязвимость из BlogEngine\.NET рассмотрим кратко, т\. к\. на подробный разбор понадобится целая статья\. Она, кстати, есть — прочитать можно [здесь](https://pvs-studio.ru/ru/blog/posts/csharp/0918/)\. 

BlogEngine\.NET — платформа для создания блогов, написанная на C\#\. Несколько хэндлеров блога оказались уязвимы к [XXE \(XML eXternal Entity\)](https://pvs-studio.ru/ru/blog/terms/6546/)\. Из\-за уязвимости можно похитить данные с машины, где развернут блог\. Для этого нужно на определённый URL закинуть специальным образом сконфигурированную XML'ку\.

У уязвимости XXE 2 составляющих:

* небезопасно сконфигурированный XML\-парсер;
* данные от злоумышленника, которые этот парсер разбирает\. 

Можно отслеживать только опасный парсер и выдавать предупреждение вне зависимости от того, какие данные он обрабатывает\. У такого подхода есть плюсы и минусы:

* Плюсы: диагностика становится проще, так как не зависит от трассы передачи данных\. Если анализатор не сможет отследить, как данные передаются в программе — не страшно, предупреждение всё равно будет выдано\. 
* Минусы: больше ложноположительных предупреждений\. Предупреждения будут выдаваться независимо от того, обрабатываются ли безопасные данные или нет\. 

Допустим, что мы решили всё\-таки отслеживать пользовательские данные\. Здесь на помощь опять приходит taint\-анализ\. 

Вернёмся к XXE\. CVE\-2018\-14485 из BlogEngine\.NET можно поймать так:

![1028_SAST_Under_The_Hood_ru/image10.png](https://import.viva64.com/docx/blog/1028_SAST_Under_The_Hood_ru/image10.png)

Анализатор отслеживает передачу данных из HTTP\-запроса и видит, как они передаются между переменными и методами\. В то же время анализатор следит за перемещением по программе экземпляра опасного парсера \(_request_ типа _XmlDocument_\)\. 

Вместе эти данные сходятся в вызове _request\.LoadXml\(xml\)_ — парсер с опасной конфигурацией обрабатывает пользовательские данные\.  

Теорию об XXE и подробное описание этой уязвимости собрал в статье "[Уязвимости из\-за обработки XML\-файлов: XXE в C\# приложениях в теории и на практике](https://pvs-studio.ru/ru/blog/posts/csharp/0918/)"\.

Ещё рекомендую посмотреть доклад, на основе которого и написана статья — там есть пример эксплуатации уязвимости с видео \(тайминг — [28:43](https://youtu.be/cREwHy4H3jM?t=1723)\)\.  

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

Мы рассмотрели некоторые подходы, которые используются для поиска уязвимостей, их сильные и слабые стороны\. Основная цель статьи — рассказать, _как_ SAST\-инструменты ищут уязвимости\. Однако в заключение хочу напомнить, _зачем_ они это делают\. 

1\. Количество уязвимостей растёт из года в год, что подтверждается [статистикой](https://nvd.nist.gov/vuln/search/statistics?form_type=Basic&results_type=statistics&search_type=all&isCpeNameSearch=false)\. Значит, о безопасности нужно заботиться\. 

![1028_SAST_Under_The_Hood_ru/image11.png](https://import.viva64.com/docx/blog/1028_SAST_Under_The_Hood_ru/image11.png)

2\. Чем раньше уязвимость нашли, тем легче и дешевле её исправить\. SAST помогает снижать финансовые и репутационные риски за счёт раннего обнаружения дефектов безопасности\. Подробнее эту тему я раскрыл в заметке "[Место SAST в Secure SDLC: 3 причины внедрения в DevSecOps\-пайплайн](https://pvs-studio.ru/ru/blog/posts/0937/)"\. 

![1028_SAST_Under_The_Hood_ru/image12.png](https://import.viva64.com/docx/blog/1028_SAST_Under_The_Hood_ru/image12.png)

\*\*

Напомню, что текст выше — сокращённая и адаптированная для чтения версия доклада "[Под капотом SAST: как инструменты анализа кода ищут дефекты безопасности](https://youtu.be/cREwHy4H3jM)"\. Сам доклад похож по структуре, но в нём больше примеров\.