Безопасность C++ приложений: топ-10 уязвимостей и способы защиты

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Безопасность C++ приложений: топ-10 уязвимостей и способы защиты

C++ — мощный язык системного программирования, но его гибкость и прямой доступ к памяти делают приложения уязвимыми для серьёзных атак. По данным OWASP и CWE, многие критические уязвимости в промышленном ПО (от игр до авионики) возникают именно из-за ошибок в C++ коде. В этой статье мы разберём топ-10 самых опасных уязвимостей, приведём примеры кода и покажем, как их избежать.

1. Переполнение буфера (Buffer Overflow)

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

// Уязвимый код
#include <cstring>
void vulnerable(char* input) {
    char buffer[10];
    strcpy(buffer, input); // Опасность: если input длиннее 9 символов
}
// Безопасный вариант
void secure(char* input, size_t len) {
    char buffer[10];
    if (len < sizeof(buffer)) {
        strncpy(buffer, input, sizeof(buffer) - 1);
        buffer[sizeof(buffer) - 1] = '\0';
    }
}

Защита: используйте std::string, std::vector или функции с ограничением длины (strncpy, snprintf). Включите флаги компилятора -fstack-protector-strong и -D_FORTIFY_SOURCE=2.

2. Использование после освобождения (Use-After-Free)

Обращение к памяти, которая уже была освобождена. Приводит к чтению/записи в чужую область или к выполнению произвольного кода.

// Уязвимый код
int* ptr = new int(42);
delete ptr;
*ptr = 100; // Ошибка! ptr уже невалиден
// Безопасный вариант
int* ptr = new int(42);
delete ptr;
ptr = nullptr; // Обнуляем указатель
if (ptr) *ptr = 100; // Проверка перед использованием

Защита: используйте умные указатели (std::unique_ptr, std::shared_ptr), обнуляйте сырые указатели после delete, применяйте санитайзеры (-fsanitize=address).

3. Целочисленное переполнение (Integer Overflow)

Выход за границы допустимого диапазона при арифметических операциях. Часто используется для обхода проверок размера буфера.

// Уязвимый код
size_t size = 0;
size_t user_input = 4294967295; // 2^32 - 1
size_t total = size + user_input + 1; // Переполнение!
// Безопасный вариант с проверкой
if (user_input > SIZE_MAX - size) {
    throw std::overflow_error("Размер слишком велик");
}
size_t total = size + user_input + 1;

Защита: используйте типы с фиксированной разрядностью (uint32_t), проверяйте переполнение перед операцией, применяйте __builtin_add_overflow (GCC/Clang).

4. Разыменование нулевого указателя (Null Pointer Dereference)

Попытка обратиться к памяти по адресу NULL. Приводит к аварийному завершению программы (segfault) или к уязвимости DoS.

// Уязвимый код
void process(int* ptr) {
    *ptr = 10; // Если ptr == nullptr — краш
}
// Безопасный вариант
void process(int* ptr) {
    if (!ptr) {
        throw std::invalid_argument("Null pointer received");
    }
    *ptr = 10;
}

Защита: всегда проверяйте указатели на nullptr перед использованием, возвращайте std::optional вместо сырых указателей.

5. Утечки памяти (Memory Leaks)

Потеря доступа к выделенной памяти без её освобождения. Со временем истощает ресурсы системы.

// Уязвимый код
void leak() {
    int* data = new int[100];
    // Забыли delete[] data — утечка
}
// Безопасный вариант
void no_leak() {
    std::unique_ptr<int[]> data = std::make_unique<int[]>(100);
    // Память освободится автоматически
}

Защита: используйте RAII (умные указатели, контейнеры STL), применяйте valgrind или санитайзеры для обнаружения утечек.

6. Гонки данных (Data Races / Race Conditions)

Неконтролируемый одновременный доступ к общим данным из разных потоков. Приводит к повреждению данных и непредсказуемому поведению.

// Уязвимый код (многопоточный)
int counter = 0;
void increment() { ++counter; } // Не атомарно
// Безопасный вариант
std::atomic<int> counter{0};
void increment() { counter.fetch_add(1); }

Защита: используйте std::atomic, мьютексы (std::mutex), lock-free структуры данных, избегайте глобальных переменных.

7. Уязвимости форматной строки (Format String Vulnerabilities)

Передача пользовательского ввода в качестве аргумента форматной строки. Позволяет читать/записывать стек.

// Уязвимый код
printf(user_input); // Если user_input = "%x %x %x" — утечка стека
// Безопасный вариант
printf("%s", user_input); // Фиксированный формат

Защита: никогда не передавайте пользовательский ввод как первый аргумент printf/sprintf. Используйте std::format (C++20) или std::string.

8. Переполнение стека (Stack Overflow)

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

// Уязвимый код
int factorial(int n) {
    return n * factorial(n - 1); // Нет базового случая
}
// Безопасный вариант (итеративный)
int factorial(int n) {
    int result = 1;
    for (int i = 2; i <= n; ++i) result *= i;
    return result;
}

<

Блоги

Рекомендаций по книгам