Статический анализ безопасности приложений
Зачем нужно автоматическое код-ревью, какие ошибки и уязвимости оно находит, чем SAST отличается от обычного статанализа и как организовать его внедрение в соответствии с ГОСТ Р 71207-2024.
Зачем нужен статический анализ
За последние 30 лет программное обеспечение прошло путь от десятков тысяч строк кода до миллионов и десятков миллионов. Вместе с размером кодовых баз изменилась и природа ошибок в них.
Кодовые базы растут — плотность ошибок тоже
Промышленные исследования показывают: чем больше проект, тем выше типичная плотность дефектов на тысячу строк кода. Это нелинейная зависимость — после некоторого порога ошибки начинают «множиться» быстрее, чем растёт сам код.
| Размер проекта (строк кода) | Типичная плотность ошибок (на 1 000 строк) |
|---|---|
| менее 2 000 | 0–25 |
| 2 000 – 16 000 | 0–40 |
| 16 000 – 64 000 | 0,5–50 |
| 64 000 – 512 000 | 2–70 |
| 512 000 и более | 4–100 |
Объём контролировать вручную становится физически невозможно. К тому же появляются разные классы ошибок — те, что ловят тесты, и те, что не ловят; критичные для безопасности и косметические; явные и скрытые.
Стоимость исправления уязвимости растёт экспоненциально
По данным NIST (National Institute of Standards and Technology), стоимость исправления одной и той же ошибки на разных стадиях жизненного цикла программного обеспечения отличается в десятки и сотни раз.
Главный вывод: чем раньше обнаружена ошибка, тем дешевле её исправление. Поэтому имеет смысл сдвигать поиск ошибок «влево» — на самые ранние этапы разработки. Статический анализ работает уже на этапе написания кода и поэтому даёт максимальную экономию.
Способы поиска ошибок
Статический анализ — лишь один из методов поиска ошибок. Он дополняет, а не заменяет другие практики обеспечения качества.
Эти методы работают вместе. Статический анализ закрывает класс ошибок, который сложно поймать тестами; тесты находят логические дефекты, которые нельзя обнаружить статически. В зрелом процессе разработки используются все методы.
Что такое статический анализ
В начале был код-ревью — ручной просмотр кода коллегами. Статический анализ — это автоматизированное код-ревью, которое выполняет специальный инструмент.
Определение по ГОСТ Р 71207-2024
Статический анализ — вид работ по инструментальному исследованию программы, основанный на анализе исходных кодов в режиме, не предусматривающем реального выполнения кода, и выполняемый для определения свойств программы.
(п. 3.1.33)
Ключевые свойства
Нужен только код
Не требуется работающее окружение, тестовые данные или сценарии исполнения. Достаточно исходных файлов проекта.
Полное покрытие
Анализатор просматривает каждый файл, каждую функцию, каждую ветку. Не нужно «угадывать», какие сценарии важны.
Раннее обнаружение
Запуск можно встроить прямо в IDE или в pre-commit hook — ошибка обнаруживается ещё до того, как код попадёт в репозиторий.
Исправление до тестирования
Ошибки устраняются ещё на этапе разработки, когда стоимость исправления минимальна — а значит, до этапа тестирования и эксплуатации они просто не доходят.
Найди ошибку: шесть реальных кейсов
Шесть фрагментов кода из открытых проектов. В каждом есть ошибка, которую сложно заметить глазом, но статический анализатор обнаруживает её за миллисекунды. Прочитайте код, попытайтесь найти проблему сами, а затем раскройте подсказку.
public void SetCredentials(string userName, string password, string domain)
{
if (string.IsNullOrEmpty(userName))
{
throw new ArgumentException("Empty user name.", "userName");
}
if (string.IsNullOrEmpty("password"))
{
throw new ArgumentException("Empty password.", "password");
}
CredentialsUserName = userName;
CredentialsUserPassword = password;
CredentialsDomain = domain;
}
string.IsNullOrEmpty("password") всегда равно false. Вместо переменной password в проверку попал её строковый литерал — кавычки скопировали из соседней строки. Проверка ничего не делает, и пустой пароль молча принимается.
public bool Equals(BlobSasBuilder other) =>
BlobName == other.BlobName &&
CacheControl == other.CacheControl &&
BlobContainerName == other.BlobContainerName &&
ContentDisposition== other.ContentDisposition &&
ContentEncoding == other.ContentEncoding &&
ContentLanguage == other.ContentEncoding &&
ContentType == other.ContentType &&
ExpiryTime == other.ExpiryTime &&
Identifier == other.Identifier &&
IPRange == other.IPRange &&
Permissions == other.Permissions &&
Protocol == other.Protocol &&
StartTime == other.StartTime &&
Version == other.Version;
ContentLanguage объект сравнивается не с other.ContentLanguage, а с other.ContentEncoding (повтор предыдущей строки). Классическая ошибка copy-paste, типичная для длинных перечислений. Глазами в 14 одинаковых строках такое не находится — а анализатор видит мгновенно.
if (request.PaymentTolerance < 0 && request.PaymentTolerance > 100)
{
ModelState.AddModelError(
nameof(request.PaymentTolerance),
"PaymentTolerance can only be between 0 and 100 percent"
);
}
false. Условие требует, чтобы число одновременно было меньше нуля и больше 100 — это невозможно. Должно быть || (или) вместо && (и). Сообщение об ошибке валидации никогда не покажется, и невалидные значения молча принимаются.
protected static bool IsFinite(float f)
{
if ( f == Mathf.Infinity
|| f == Mathf.NegativeInfinity
|| f == float.NaN)
{
return false;
}
return true;
}
float.NaN бессмысленно. По стандарту IEEE 754 любое сравнение с NaN (включая NaN == NaN) возвращает false. Третья ветка условия никогда не сработает: NaN-значения молча проскальзывают через IsFinite и считаются «конечными». Правильный способ — float.IsNaN(f).
var firstUrl = new Uri(urlHook, UriKind.Absolute);
var secondUrl = new Uri(url, UriKind.Absolute);
return firstUrl.Scheme == secondUrl.Scheme
&& firstUrl.Port == secondUrl.Port
&& firstUrl.Host == firstUrl.Host;
firstUrl.Host слева и справа от оператора ==. На последней строке должно быть secondUrl.Host. Сравнение объекта самого с собой всегда возвращает true, поэтому хост из проверки фактически выпадает — webhook принимает запросы с любого хоста, лишь бы совпали схема и порт. Это потенциальная уязвимость безопасности.
ParagraphFormat paragraphFormat;
public ParagraphFormat ParagraphFormat
{
get { return paragraphFormat; }
set { ParagraphFormat = value; }
}
ParagraphFormat [CWE-674: Uncontrolled Recursion]. В сеттере вместо приватного поля paragraphFormat с маленькой буквы используется само свойство ParagraphFormat с большой. Запись свойства приводит к вызову того же сеттера — переполнение стека. Это уже не просто ошибка качества, а классифицированная по CWE уязвимость.
Что объединяет все шесть кейсов. Это не «дурацкие ошибки начинающих» — это код из реальных открытых проектов: OnlyOffice, Azure SDK, BTCPay Server, UnityCsReference, eShopOnContainers, FastReport. Их написали профессионалы, прошёл код-ревью, проект используется в production. И всё равно — внимание разработчика конечно, а статический анализатор не устаёт.
Safety и Security: две разные «безопасности»
В русском языке оба слова переводятся как «безопасность», но в инженерном контексте это разные дисциплины — с разной целью и разными стандартами.
Безопасность
SafetyНадёжная работа приложения в любых условиях, без вмешательства извне. Цель — чтобы программа не сбойнула сама по себе, не повредила данные, не привела к аварии в физическом мире.
Типичная сфера: авионика, автомобилестроение, медицинская электроника, промышленная автоматика.
Защищённость
SecurityСтойкость ко внешним воздействиям и попыткам вмешательства. Цель — чтобы атакующий не смог использовать программу против её пользователей, не похитил данные, не получил несанкционированный доступ.
Типичная сфера: веб-приложения, мобильные приложения, корпоративные системы, ИТ-инфраструктура.
Граница не строгая. Многие дефекты попадают в обе категории: например, переполнение буфера может привести и к падению (safety), и к удалённому исполнению кода (security). Современный SAST-инструмент обычно работает в обеих плоскостях.
Что такое SAST
SAST — Static Application Security Testing. Это статический анализ, специально нацеленный на поиск потенциальных уязвимостей.
Уязвимость как особый класс ошибок
Любая уязвимость — это ошибка в коде, но не любая ошибка — уязвимость. Разделение проходит по последствиям: если ошибка может быть использована атакующим для нарушения безопасности обрабатываемой информации (получение доступа, утечка данных, выполнение чужого кода), то она классифицируется как критическая.
Критическая ошибка — ошибка, которая может привести к нарушению безопасности обрабатываемой информации (ГОСТ Р 71207-2024, п. 3.1.13).
Стандарт намеренно не разграничивает ошибки по последствиям — важен сам факт наличия и необходимости исправления.
Свойства SAST
SAST наследует все полезные свойства статического анализа:
- Нужен только исходный код — не требуется развёрнутое приложение или тестовые данные.
- Полное покрытие — анализируются все ветки исполнения, включая редко срабатывающие.
- Раннее обнаружение — ещё на этапе разработки, до этапа тестирования.
- Исправление до того, как уязвимость попадёт в эксплуатацию — пока стоимость минимальна.
CWE и CVE: каталоги уязвимостей
Чтобы говорить об уязвимостях на одном языке, индустрия выработала два стандартных каталога: один для типов слабостей, другой — для конкретных найденных уязвимостей.
CWE
Common Weakness EnumerationКаталог типов слабостей. Описывает категории ошибок, которые в принципе могут привести к уязвимости — но абстрактно, без привязки к конкретному продукту.
Например, CWE-89 — SQL-инъекция, CWE-674 — неконтролируемая рекурсия, CWE-119 — переполнение буфера. Это потенциальные уязвимости.
CVE
Common Vulnerabilities and ExposuresКаталог конкретных уязвимостей. Каждая запись — это отдельная уязвимость в конкретной версии конкретного продукта, найденная и зарегистрированная.
Например, CVE-2021-44228 — log4shell в Apache Log4j 2.x. Уязвимость уже существует и эксплуатируется.
Уязвимостей становится больше
Число регистрируемых CVE растёт каждый год — и темп ускоряется. По данным cvedetails.com и CVE.org, за последнее десятилетие количество ежегодных публикаций уязвимостей увеличилось более чем в семь раз: с ~6,5 тысяч в 2015 году до ~48 тысяч в 2025-м.
Пример SAST-предупреждения: SQL-инъекция
Рассмотрим, как SAST находит классическую уязвимость — SQL-инъекцию. Ключевая идея SAST — отслеживание помеченных данных (taint analysis): данные от пользователя помечаются как «опасные» и анализатор следит за их распространением по программе.
Уязвимый код
using (SqlConnection connection = new SqlConnection(....))
{
....
String userName = Request.Form["userName"]; // SOURCE — данные от пользователя
using (var command = new SqlCommand()
{
Connection = connection,
CommandText = "SELECT * FROM Users WHERE UserName = '"
+ userName // TAINT — конкатенация в SQL
+ "'",
CommandType = System.Data.CommandType.Text
})
{
var reader = command.ExecuteReader(); // SINK — выполнение запроса
}
}
Что происходит
- Источник (source) —
Request.Form["userName"]: данные пришли от пользователя, доверять им нельзя. - Распространение (taint flow) — переменная
userNameподставляется напрямую в строку SQL-запроса через конкатенацию+. - Сток (sink) — итоговая строка передаётся в
SqlCommand.CommandTextи исполняется как SQL.
Атакующий может ввести в форму строку вида ' OR '1'='1' -- и превратить её в произвольный SQL-запрос, обходя авторизацию или вытаскивая данные из других таблиц.
V5608 Possible SQL injection. Potentially tainted data in the 'userName' variable is used to create SQL command.
Анализатор отследил весь маршрут от источника пользовательских данных до места их использования в SQL-запросе и автоматически выдал предупреждение. Чтобы исправить — использовать параметризованные запросы (SqlParameter).
Этот же подход (taint-анализ) лежит в основе поиска большинства injection-уязвимостей: Command injection, Path traversal, XSS, LDAP injection и т. д. Везде SAST ищет связку source → flow → sink с проверкой санитизации на пути.
Регуляторный контекст
В России разработка безопасного ПО (РБПО) и применение статического анализа регулируются национальными стандартами серии ГОСТ Р. Их соблюдение — обязательное условие для поставщиков средств защиты информации и значительной части государственных и корпоративных заказчиков.
Эволюция стандартов РБПО
За девять лет — от первой редакции базового стандарта до обновлённой версии 2024 года, дополненной специализированным стандартом по статическому анализу.
Кто разработал
ГОСТ Р 71207-2024 разработан совместно:
- ФСТЭК России — Федеральная служба по техническому и экспортному контролю.
- ИСП РАН — Институт системного программирования им. В. П. Иванникова Российской академии наук.
Стандарт внесён ТК 362 «Защита информации», утверждён приказом Росстандарта от 18 января 2024 года № 25-ст и введён в действие с 1 апреля 2024 года.
ГОСТ Р 71207 применяется совместно с ГОСТ Р 56939. Стандарт не отменяет базовый, а дополняет его — конкретизирует процесс статического анализа в рамках общего жизненного цикла РБПО.
Важный нюанс редакций. ГОСТ Р 71207-2024 был утверждён в январе 2024 года и в своих нормативных ссылках упоминает ГОСТ Р 56939 без указания года — то есть действующую на момент применения редакцию. На момент публикации 71207 это была версия 2016 года; с 20 декабря 2024 года применяется редакция 2024 года.
Кому адресован стандарт
- Разработчикам статических анализаторов — задаёт обязательный набор реализуемых методов анализа и качественные показатели.
- Разработчикам средств защиты информации и средств обеспечения безопасности ИТ — задаёт требования к процессу применения статанализа.
- Разработчикам ПО общего назначения, поставляющим продукты под требования РБПО.
Внедрение статического анализа по ГОСТ Р 71207
Стандарт предписывает чёткую схему внедрения: три последовательных этапа и набор контрольных сроков для регулярной работы.
Три этапа внедрения
Подготовительный
- Выбор инструмента (или набора инструментов) статического анализа
- Анализ документации проекта, языков программирования, систем сборки
- Подготовка сборочной среды и среды анализа
Начальный
- Настройка инструмента под конкретный проект
- Первичный запуск статического анализа
- Разметка результатов: истинные / ложные / не требующие исправления предупреждения
- Формирование начальной базы предупреждений (baseline)
Регулярный
- Постоянный анализ всего кода и изменений
- Интеграция в систему непрерывной интеграции (CI)
- Регулярный пересмотр конфигурации и настроек
- Контроль устранения подтверждённых ошибок
Контрольные сроки регулярного анализа
Стандарт устанавливает чёткие временные рамки, которые превращают анализ из эпизодического запуска в встроенную часть процесса разработки:
Что это значит на практике. Если у вас нет CI-системы, выполняющей статический анализ на каждый коммит, вы не сможете уложиться в сроки 71207. Поэтому стандарт неявно предписывает наличие зрелого процесса DevOps с автоматизацией.
Критические ошибки по ГОСТ Р 71207
Раздел 6 стандарта устанавливает обязательный перечень типов критических ошибок, которые должен искать статический анализатор. Список зависит от языка программирования.
п. 6.3
Компилируемые языки: общие типы
Для всех компилируемых языков (C, C++, Rust, Go, Pascal, Ada и т. п.) обязательны следующие типы критических ошибок:
- Непроверенное использование чувствительных данных — ввод пользователя, данные из файлов, сети и т. п. без должной валидации.
- Целочисленное переполнение и некорректное совместное использование знаковых и беззнаковых чисел.
- Переполнение буфера — запись или чтение за пределами выделенной памяти.
- Некорректное использование системных процедур и интерфейсов безопасности — шифрование, разграничение доступа и пр.
- Ошибки многопоточности — гонки данных, некорректная синхронизация, deadlock'и.
п. 6.4
Интерпретируемые языки
Для интерпретируемых языков (Python, JavaScript, PHP, Ruby, Perl и т. п.) перечень короче — отсутствуют пункты, специфичные для управления памятью и низкоуровневой арифметики:
- Непроверенное использование чувствительных данных.
- Некорректное использование интерфейсов безопасности — крипто, ACL.
- Ошибки работы с многопоточными примитивами.
п. 6.5
C и C++: дополнительные типы
Для C и C++ к общему списку добавляются специфические для языка типы — все они связаны с ручным управлением памятью и слабой типизацией:
- Разыменование нулевого указателя.
- Деление на ноль.
- Ошибки управления динамической памятью — выделение, освобождение, использование освобождённой памяти (use-after-free).
- Ошибки использования форматной строки — небезопасные
printf-подобные функции. - Использование неинициализированных переменных.
- Утечки памяти, незакрытые файловые дескрипторы и сетевые соединения.
п. 7.4
Обязательные методы анализа
Анализатор должен реализовывать все следующие методы:
- Внутрипроцедурный анализ потоков данных и управления.
- Межпроцедурный и межмодульный контекстно-чувствительный анализ потока данных.
- Чувствительный к путям выполнения анализ потоков данных и управления.
- Межпроцедурный и межмодульный контекстно-чувствительный анализ помеченных данных (taint analysis).
- Анализ программы на синтаксическом уровне.
Дополнительно рекомендуется включать: сигнатурный поиск, анализ псевдонимов, анализ косвенных вызовов, статистический анализ, анализ иерархии классов.
Требования к инструментам и специалистам
Стандарт предъявляет конкретные численные и функциональные требования — как к самому статическому анализатору, так и к людям, которые с ним работают.
Чек-лист для инструмента (п. 8)
- Качество анализа: доля ложноположительных срабатываний (FP) ≤ 50%, доля ложноотрицательных (FN, пропусков) ≤ 50% — на квалификационном наборе тестов.
- Скорость: полный анализ ПО с заимствованными компонентами на средствах разработчика — не более 2 суток.
- Поддержка заимствованных компонентов: анализ должен охватывать ПО целиком, включая сторонние библиотеки.
- Состав предупреждения: описание ошибки, тип, точное место в исходном коде, рекомендация по исправлению.
- Связь с CWE: для каждого типа ошибки в документации указано соответствие одному или нескольким идентификаторам MITRE CWE.
- Открытый формат вывода: результаты анализа доступны в машиночитаемом формате — например,
SARIF(Static Analysis Results Interchange Format). - Автоматизация: доступен консольный или программный интерфейс для CI/CD-интеграции.
- Хранение и сравнение результатов: сохранение запусков с уникальными идентификаторами предупреждений и сравнение между запусками.
- Поддержка разметки: возможность пометить предупреждение как «истинное», «ложное», «не требует исправления» с автоматизацией повторных решений.
- Документация: описание всех типов ошибок с причинами, примерами кода и способами исправления.
Две роли специалистов (п. 9)
DevOps / системное администрирование
Внедрение и сопровождение анализаОтвечает за инфраструктуру: настройку среды, интеграцию в CI, ресурсное планирование. Участвует в подразделах 5.3 и 5.6 стандарта.
- Программная инженерия и жизненный цикл ПО
- Архитектура ЭВМ и устройство технических средств
- Сетевые протоколы и сервисы
- Оценка временных и вычислительных ресурсов
Безопасность ПО / разметка предупреждений
Анализ результатов и устранениеОтвечает за содержательную часть: выбор анализатора, настройку, экспертизу предупреждений, оценку влияния на безопасность. Подразделы 5.2, 5.4, 5.7–5.12.
- Основы безопасности ПО и систем
- Языки программирования анализируемого ПО
- Алгоритмы и теория сложности
- POSIX и устройство ОС
- Опыт работы со статическими анализаторами или повышение квалификации
Интеграция в процесс разработки
Современный SAST-инструмент перестаёт быть «отдельной программой, в которую раз в месяц приходят». Он встраивается во все ключевые точки разработки — от IDE до облачного CI.
Ниже — карта экосистемы интеграций на примере PVS-Studio. Аналогичный список характерен для большинства зрелых SAST-решений.
IDE и редакторы кода
Системы сборки
Платформы качества кода
Облачные и локальные CI
Игровые движки
Embedded-платформы
Виртуализация и распределённая сборка
Что это значит на практике. Разработчик видит предупреждения прямо в IDE при наборе кода. CI-пайплайн блокирует merge-request, если в изменённых файлах появились новые критические предупреждения. Платформа качества (SonarQube / DefectDojo) агрегирует результаты со всех проектов в едином дашборде. Получается замкнутый контур, в котором ошибка не может незаметно «просочиться» в production.