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

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

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

Начать курс

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

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

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

Классика жанра. Эта уязвимость возникает, когда программа записывает данные за границы выделенного буфера. Злоумышленник может перезаписать соседние участки памяти, включая адрес возврата функции, и получить контроль над исполнением кода.

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

// Безопасная версия
void safe() {
    char buffer[10];
    std::cout << "Введите строку: ";
    std::cin.getline(buffer, sizeof(buffer));
    std::cout << buffer << std::endl;
}

Как защититься: Всегда используйте функции с ограничением длины (strncpy, snprintf, std::string). Избегайте gets() и strcpy().

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

Если вы выделили память через new, но забыли освободить её через delete, программа будет постепенно "съедать" оперативную память. Для серверных приложений это катастрофа — через несколько часов работы сервис упадёт с ошибкой std::bad_alloc.

// Пример утечки
void leak() {
    int* ptr = new int(42);
    // Забыли delete ptr;
}

// Правильный подход
void noLeak() {
    std::unique_ptr<int> ptr = std::make_unique<int>(42);
    // Память освободится автоматически при выходе из области видимости
}

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

3. Висячие указатели (Dangling Pointers)

Указатель ссылается на уже освобождённую память. Разыменование такого указателя — неопределённое поведение (UB). Это может привести к крашу программы или, что хуже, к выполнению вредоносного кода.

int* dangling() {
    int x = 10;
    return &x; // Ошибка: возвращаем адрес локальной переменной!
}

// Правильно — выделить память в куче
int* safe() {
    int* ptr = new int(10);
    return ptr;
}

Как защититься: После delete всегда присваивайте указателю nullptr. Используйте умные указатели.

4. Переполнение целых чисел (Integer Overflow)

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

void check_size(size_t user_size) {
    // Уязвимость: если user_size очень большая, user_size + 1 может стать маленькой
    char* buffer = new char[user_size + 1];
    // ...
}

// Защита
void safe_check(size_t user_size) {
    if (user_size > std::numeric_limits<size_t>::max() - 1) {
        throw std::overflow_error("Размер слишком велик");
    }
    char* buffer = new char[user_size + 1];
}

Как защититься: Проверяйте границы перед арифметическими операциями. Используйте __builtin_add_overflow (GCC/Clang) или библиотеки для безопасной арифметики.

5. Null-указатели (Null Pointer Dereference)

Разыменование нулевого указателя приводит к немедленному крашу (Segmentation Fault). Хотя это не всегда уязвимость безопасности, злоумышленник может спровоцировать DoS-атаку.

void process(int* ptr) {
    *ptr = 5; // Если ptr == nullptr — краш
}

// Безопасная версия
void safe_process(int* ptr) {
    if (ptr == nullptr) {
        return; // или выбросить исключение
    }
    *ptr = 5;
}

Как защититься: Всегда проверяйте указатели на nullptr перед использованием. Используйте ссылки вместо указателей, где это возможно.

6. Состояние гонки (Race Condition)

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

int counter = 0;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        counter++; // Гонка данных!
    }
}

// Безопасно
std::atomic<int> safe_counter{0};

void safe_increment() {
    for (int i = 0; i < 100000; ++i) {
        safe_counter.fetch_add(1, std::memory_order_relaxed);
    }
}

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

7. Неправильная обработка строк (String Handling)

C-строки (массивы char) — источник многих проблем. Отсутствие завершающего нуля, путаница с кодировками — всё это ведёт к уязвимостям.

// Опасный код
void copy_string(const char* src) {
    char dest[100];
    strcpy(dest, src); // Если src длиннее 99 символов — переполнение
}

// Безопасно
void safe_copy(const std::string& src) {
    std::string dest = src; // std::string сам управляет памятью
}

Как защититься: Используйте std::string вместо C-строк. Если без C-строк не обойтись — применяйте strncpy

Блоги

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