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

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

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

Начать курс

Введение: почему безопасность C++ кода критична?

C++ — один из самых мощных и производительных языков, но его гибкость требует от разработчика высокой ответственности. В отличие от многих современных языков с автоматическим управлением памятью (Java, Python), C++ предоставляет прямой доступ к памяти и системным ресурсам. Это открывает широкие возможности для атак: от переполнения буфера до утечек данных.

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

Топ-10 уязвимостей C++ с примерами и защитой

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

Проблема: Запись данных за границы выделенного массива. Это классика C/C++, которая позволяет перезаписать соседние переменные, адрес возврата функции или данные в куче.

// Пример уязвимости
#include <cstring>
#include <iostream>

void vulnerable() {
    char buffer[10];
    std::cout << "Введите строку: ";
    std::cin >> buffer; // Опасность! Нет проверки длины
}

int main() {
    vulnerable();
    return 0;
}

Последствия: Выполнение произвольного кода (RCE), крах программы.

Защита: Используйте std::string, std::vector, безопасные функции (например, strncpy с указанием размера).

// Безопасный вариант
#include <string>
#include <iostream>

void safe() {
    std::string input;
    std::getline(std::cin, input);
    std::cout << "Вы ввели: " << input << std::endl;
}

2. Утечка памяти (Memory Leak)

Проблема: Память выделена через new/malloc, но не освобождена через delete/free. Со временем приложение «съедает» всю оперативную память.

void leaky() {
    int* data = new int[1000];
    // Забыли delete[] data;
}

Последствия: Отказ в обслуживании (DoS), замедление системы.

Защита: Используйте умные указатели (std::unique_ptr, std::shared_ptr), RAII (Resource Acquisition Is Initialization).

#include <memory>
void safe() {
    std::unique_ptr<int[]> data = std::make_unique<int[]>(1000);
    // Память освободится автоматически при выходе из функции
}

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

Проблема: Попытка обратиться к памяти по адресу nullptr.

void process(int* ptr) {
    *ptr = 42; // Если ptr == nullptr, программа упадет
}

Последствия: Немедленный крах (segmentation fault), DoS.

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

void safe(int* ptr) {
    if (ptr) {
        *ptr = 42;
    } else {
        // Обработка ошибки
    }
}

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

Проблема: Результат арифметической операции выходит за пределы типа данных. Это может привести к непредсказуемому поведению, особенно при работе с индексами массивов.

#include <cstdint>
void vulnerable() {
    uint16_t a = 65535;
    uint16_t b = 1;
    uint16_t sum = a + b; // Переполнение: 65536 -> 0
}

Последствия: Логические ошибки, переполнение буфера (если используется для вычисления размера).

Защита: Используйте типы с фиксированной размерностью (int32_t, int64_t), проверяйте границы. В C++17 есть std::clamp и std::numeric_limits.

#include <limits>
bool safe_add(uint16_t a, uint16_t b, uint16_t& result) {
    if (a > std::numeric_limits<uint16_t>::max() - b) {
        return false; // Переполнение
    }
    result = a + b;
    return true;
}

5. Форматная строка (Format String Vulnerability)

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

void print_user_input(const char* user_input) {
    printf(user_input); // Огромная уязвимость!
}

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

Защита: Никогда не передавайте пользовательский ввод в первый аргумент printf.

void safe(const char* user_input) {
    printf("%s", user_input); // Правильно
}

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

Проблема: Обращение к памяти, которая уже была освобождена. Указатель становится «висячим».

int* ptr = new int(10);
delete ptr;
*ptr = 20; // Ошибка! Память уже не наша

Последствия: Повреждение данных, RCE, крах.

Защита: Обнуляйте указатели после delete (ptr = nullptr;), используйте умные указатели.

std::unique_ptr<int> ptr = std::make_unique<int>(10);
// Память будет освобождена, когда ptr выйдет из области видимости
// Доступ к ptr-> будет невозможен после освобождения

7. Гонка данных (Data Race / Race Condition)

Проблема: Два потока одновременно читают и пишут в одну переменную без синхронизации.

<

Блоги

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