Как часто анализировать проект? Сколько анализаторов использовать? Как размечать полученные предупреждения? Отвечаем на эти и другие вопросы, разбираясь в подробностях свежего ГОСТ Р 71207-2024, посвящённого статическому анализу.

В 2016 году вышел ГОСТ Р 56939-2016, посвящённый общим требованиям к разработке безопасного программного обеспечения (РБПО). Понятно, что он весь посвящен различным аспектам создания безопасных приложений, однако нам, как разработчикам статического анализатора кода, более всего был интересен пункт 3.15 — статический анализ исходного кода программы:

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

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

Но 1 апреля 2024 (и это не первоапрельская шутка) появился ГОСТ Р 71207–2024, целиком посвящённый использованию статического анализа в процессе разработки безопасного программного обеспечения.

В этой статье мы разберём самое интересное и важное из ГОСТ Р 71207–2024 на примере анализатора PVS-Studio, который разрабатывается с учётом требований, предъявляемых в этом стандарте. Это поможет понять, что нужно изменить в использовании статического анализа при разработке, чтобы соответствовать новому ГОСТу.

Определения

В стандарте даётся множество определений, начиная от технологий статического анализа, заканчивая понятиями непосредственно из программирования.

Например, пункт 3.1.3 — анализ помеченных данных (или taint-анализ) — это статический анализ, при котором анализируется течение потока данных от источников до стоков.

Примечание. Подробнее о taint-анализе можно прочитать в нашей терминологии по ссылке.

Нам как разработчикам статического анализатора и так было известно это определение, как и людям, которые тесно взаимодействуют со статическим анализом в своей работе. Однако на этапе принятия решения о внедрении или при знакомстве с технологиями статического анализа не всегда легко понять, что именно значат те или иные термины. А с выходом этого ГОСТа появился один конкретный источник, из которого можно узнать, что есть что.

Ещё одно полезное и важное определение содержится в пункте 3.1.13 — критическая ошибка в программе — это ошибка, которая может привести к нарушению безопасности обрабатываемой информации. Мы ранее называли такие ошибки потенциальными уязвимостями. Касательно данного определения важно сказать, что стандарт не разграничивает ошибки в части последствий. Важен сам факт существования и необходимости исправления подобных ошибок. Мы периодически упоминали об этом в своих статьях. Очень приятно было прочитать об этом и в ГОСТе.

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

Критические ошибки

Критические ошибки в этом стандарте группируют по типам в зависимости от языка программирования. Рассмотрим общие типы для компилируемых языков на примерах ошибок, найденных с помощью PVS-Studio.

Есть такой фрагмент кода из проекта FreeSwitch:

static const char *basic_gets(int *cnt)
{
  ....
  int c = getchar();
  if (c < 0) {
    if (fgets(command_buf, sizeof(command_buf) - 1, stdin) != command_buf) {
      break;
    }
    command_buf[strlen(command_buf)-1] = '\0';    <=
    break;
  }
  ....
}

Предупреждение: V1010 Unchecked tainted data is used in index: 'strlen(command_buf)'.

Анализатор ругается на обращение по индексу к массиву command_buf, поскольку в качестве индекса используются непроверенные внешние данные, полученные через функцию fgets из потока stdin. При определённых условиях здесь может произойти запись '\0' за пределы массива, что приведёт к неопределённому поведению. По стандарту подобные ошибки называются ошибками непроверенного использования чувствительных данных.

Ошибки второго типа называются ошибками целочисленного переполнения и некорректного совместного использования знаковых и беззнаковых чисел. Например, такой фрагмент кода из LLVM:

template <typename T>
void scaleShuffleMask(int Scale, ArrayRef<T> Mask, SmallVectorImpl<T> &ScaledMask) {
  assert(0 < Scale && "Unexpected scaling factor");
  int NumElts = Mask.size();
  ScaledMask.assign(static_cast<size_t>(NumElts * Scale), -1);    <=
  ....
}

Предупреждение: V1028 Possible overflow. Consider casting operands of the 'NumElts * Scale' operator to the 'size_t' type, not the result.

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

А в следующем фрагменте кода из проекта LibreOffice анализатор нашёл ошибку, относящуюся к типу ошибок переполнения буфера:

typedef struct {
  ....
  WCHAR wszTitle[MAX_COLUMN_NAME_LEN];
  WCHAR wszDescription[MAX_COLUMN_DESC_LEN];
} SHCOLUMNINFO, *LPSHCOLUMNINFO;

HRESULT STDMETHODCALLTYPE CColumnInfo::GetColumnInfo(
  DWORD dwIndex, SHCOLUMNINFO *psci)
{
  ....
  wcsncpy(psci->wszTitle,
          ColumnInfoTable[dwIndex].wszTitle,
          (sizeof(psci->wszTitle) - 1));          <=
  return S_OK;
}

Предупреждение: V512 A call of the 'wcsncpy' function will lead to overflow of the buffer 'psci->wszTitle'.

Здесь разработчики при вызове wcsnpy забыли поделить sizeof(psci->wszTitle) на размер одного широкого символа типа WCHAR, что и может привести к переполнению буфера.

Следующий тип критических ошибок — ошибки некорректного использования системных процедур и интерфейсов, связанных с обеспечением информационной безопасности. Как ясно из названия, такие ошибки непосредственно касаются обеспечения информационной безопасности. Посмотрим на пример кода из проекта Crypto++:

MicrosoftCryptoProvider::MicrosoftCryptoProvider()
{
  if(!CryptAcquireContext(&m_hProvider, 0, 0, PROV_RSA_FULL,
                          CRYPT_VERIFYCONTEXT))         <=
    throw OS_RNG_Err("CryptAcquireContext");
}

Предупреждение: V1109 The 'CryptAcquireContextA' function is deprecated. Consider switching to an equivalent newer function.

Здесь анализатор сообщил о том, что функция CryptAcquireContext более не поддерживается и следует использовать API шифрования посвежее.

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

public class WelcomeResource {
  private AtomicBoolean shouldBootstrap;            <=

  ....

  private boolean shouldBootstrap() {
    if (shouldBootstrap == null) {
      synchronized (this) {
        if (shouldBootstrap == null) {
          shouldBootstrap = new AtomicBoolean(....);
        }
      }
    }
    return shouldBootstrap.get();
}

Предупреждение: V6082 Unsafe double-checked locking. The field should be declared as volatile.

В данном коде у поля shouldBootstrap отсутствует модификатор volatile, из-за чего данный объект может быть использован разными потоками до окончания процесса их инициализации. Вся "прелесть" данной ошибки в том, что она может показать себя не сразу. Неправильное поведение может произойти из-за сочетания различных факторов, например, используемой версии JVM.

Типы критических ошибок также могут дополняться в зависимости от используемого языка программирования. Например для C и C++ в стандарте приведённый выше список также дополняется ошибками:

  • разыменования нулевого указателя;

  • деления на ноль;

  • управления динамической памятью;

  • использования форматной строки;

  • использования неинициализированных переменных;

  • утечек памяти, незакрытых файловых дескрипторов и дескрипторов сетевых соединений.

Чтобы находить все эти ошибки в используемом статическом анализаторе должны быть реализованы:

  • анализ программы на синтаксическом уровне;

  • внутрипроцедурный анализ потоков данных и управления;

  • межпроцедурный и межмодульный контекстно-чувствительный анализ потока данных;

  • чувствительный к путям выполнения анализ потоков данных и управления;

  • межпроцедурный и межмодульный контекстно-чувствительный анализ помеченных данных.

А также рекомендуются следующие вспомогательные методы анализа:

  • сигнатурный поиск;

  • анализ псевдонимов;

  • анализ косвенных вызовов;

  • статистический анализ;

  • анализ иерархии классов.

Внедрение и выполнение статического анализа

Целый раздел ГОСТа посвящён тому, как, собственно, необходимо использовать статический анализ в своей работе.

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

Примечание. Для совместного использования PVS-Studio с другими анализаторами все диагностики классифицируются по CWE и поддерживается формат отчёта SARIF. А документацию PVS-Studio можно найти по ссылке.

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

  • долей ложноотрицательных срабатываний (анализатор ничего не увидел);

  • долей ложноположительных срабатываний (анализатор увидел, но это не ошибка);

  • скоростью анализа.

Далее необходимо подготовить сборочную среду и среду анализа программного обеспечения. По стандарту следует использовать среду анализа, позволяющую провести анализ всего кода проекта не более чем за двое суток. Анализ также можно проводить в виртуальной инфраструктуре или использовать в виде сервиса.

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

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

Примечание. В версии PVS-Studio 7.33 появилась настройка Security Related Issues для Visual Studio, которая позволяет выделить предупреждения, выявляющие критические ошибки, описанные в ГОСТ. Подробнее можно прочитать в документации.

После проведения первичного анализа полученные предупреждения размечаются в зависимости от из истинности или ложности. Стандарт предлагает разделять полученные предупреждения на следующие группы:

  • истинные предупреждения;

  • ложные предупреждения;

  • истинные, но не требующие исправлений кода предупреждения.

Однако данная классификация может быть расширена, если есть такая необходимость. Также после первичной разметки предусмотрена возможность частичной доработки конфигурации анализатора, например, можно подавлять предупреждения в макросах (C, C++) или сократить набор правил для тестов.

Примечание. У нас есть статья про то, как внедрить статический анализатор и не наломать дров. Прочитать можно по ссылке.

После завершения этого этапа также может быть принято решение о выборе другого инструмента статического анализа.

Регулярное проведение статического анализа

Этот пункт, наверное, один из самых любимых нами в этом стандарте. Мы много раз слышали истории о том, как «внедряется» статический анализ и какие интересные ситуации с ним происходят. Однако стандарт и здесь рассказывает о том, что, как и когда нужно делать.

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

Для упрощения проведения регулярного анализа можно автоматизировать процесс анализа — использовать, например, системы непрерывной интеграции.

Примечание. Анализатор PVS-Studio можно использовать, например, в DefectDojo или SonarQube. А также есть BlameNotifier, позволяющий рассылать результаты анализа по электронной почте. Подробнее можно прочитать в документации по ссылке.

В разработке статический анализ необходимо проводить не реже, чем раз в 10 дней, если за этот период исходный код был изменён, а для добавленных или изменённых частей программы проводить анализ необходимо после каждого внесённого изменения. Результаты каждого проведённого анализа должны сохраняться.

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

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

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

Заключение

Это, конечно, не всё, что описано в ГОСТ Р 71207–2024, однако я постарался выделить самое важное и интересное. Мы надеемся, что данный стандарт повлияет на то, как используется статический анализ в разработке безопасного программного обеспечения в лучшую сторону.

А получить пробную версию и попробовать статический анализатор кода PVS-Studio можно по этой ссылке.

Дополнительные ссылки

Цикл вебинаров Андрей Карпова, посвященных ГОСТ Р 71207–2024:

  1. Общее описание и актуальность (презентация)

  2. Терминология (презентация)

  3. Критические ошибки (презентация)

  4. Технологии анализа кода (презентация)

  5. Процессы (презентация)

Комментарии (0)