Привет, хабровчане. Для будущих студентов курса "C++ Developer. Professional" Александр Колесников подготовил статью.
Приглашаем также посмотреть открытый вебинар на тему Области видимости и невидимости. За 1,5 часа участники вместе с экспертом успеют реализовать класс общего назначения и запустить несколько unit-тестов с использованием googletest. Присоединяйтесь.

В статье на примерах будет рассмотрено, почему приложения на языке программирования С++ стоит разрабатывать с особым вниманием.
Сегодня язык программирования С++ существует в нескольких
параллельных реальностях: C++98, C++11, C++14, C++17, C++20.
Существует как минимум один
источник, где можно немного разобраться со всем этим набором
мультивселенных. Однако, когда дело дойдет до написания кода
использования stackOverflow, вопрос а точно эта строка
написана безопасно", будет мучать разработчика из релиза в релиз.
Кстати, на момент написания статьи готовится новый стандарт С++23
=).
Откуда проблемы
С++ это весьма мощный язык программирования, который позволяет разрабатывать приложения для любого уровне операционной системы. Он существует уже не оно десятилетие и программист должен понимать, что делает, поскольку любая строка в коде может нести в себе довольно большое количество подводных камней, которые в будущем могут привести к проблемам с безопасностью кода. Каждое новое обновление увеличивает вероятность столкнуться с такими вещами.
Самые распространенные проблемы, с которыми может столкнуться новичок:
-
неверное объявление типов данных;
-
неверное использование выражений;
-
неправильная обработка целочисленных данных;
-
неправильная работа с контейнерами;
-
неправильная работа со строками;
-
неправильная работа с памятью;
-
неверная обработка exception;
-
пренебрежение ограничениями OOP;
-
состояния гонки при обработке ресурсов мультипоточным приложением;
-
прочие проблемы, о которых мало, где сказано.
Похоже, что если вчитаться в список ограничений, то можно
подумать, что С++ это сплошное нельзя и вроде проканает,
запустится и будет работать. Безусловно, можно сконцентрироваться
на алгоритме и не обращать внимание на все описанные выше проблемы,
но, к сожалению, большое количество CVE, которые заводят именно на
приложения, написанные на С++, зашкаливает.
Разберем несколько примеров.
String Format
void check_password(const char *user) { int ret; // static const char format[] = "%s wrong pass.\n"; size_t messageLength = strlen(user) + sizeof(msg_format); char *data = (char *)malloc(messageLength); // <- так же не очень безопасный вариант if (data == NULL) { //Код для ошибки } ret = snprintf(data, messageLength, format, user); if (ret < 0) { //Код для ошибки } else if (ret >= messageLength) { //Последний шанс обработать некорректные данные } syslog(LOG_INFO, msg); free(msg);}
В чем проблема? Данные, которые используются для создания строки, контролируются пользователем. Передача других спец символов (%n, %x) и использование строк больших размеров может вывести из строя приложение.
Integer overflow
Данная проблема головная боль любого ПО, которое работает с
накапливаемыми данными. Какого размера переменные использовать,
чтобы оптимально хранить данные и одновременно сделать запас для
приложения, если если оно будет использоваться месяцами без
перезапуска? В некоторых случаях ответить однозначно на этот вопрос
нельзя, поэтому программист, ориентируясь на собственный опыт,
волевым решением пишет uint16_t
. Но данных оказывается
больше 65,535 и тут случается чудо значение переменной становится
равно нулю и отсчет идёт заново. Пример кода:
...user->nameLength = getUserNameLength(&user->name) ;user->newDbCellLen = malloc(user->nameLength * sizeof(uint8_t))...
Одна строка и один выстрел в голову всему приложению. Теперь пользователь может спокойно выделять столько памяти, сколько ему нужно. При этом приложение продолжит какое-то время работать. Исправить можно с помощью простой функции:
...int16_t checkLen(uint16_t firstNumber, uint16_t secondNumber){ uint16_t resultLength; if (UINT_MAX - firstNumber < secondNumber) { //ошибка return -1; } else { resultLength = firstNumber + secondNumber; } return resultLength;}
Преобразование типов
Если вас не пугают сложности и вы все-таки хотите развиваться как программист С++, то скорее всего к вам в руки попадет код, который разрабатывался Когда динозавры под стол пешком ходили и в нем будет много кода, аналогичному приведенному ниже:
...unsigned int number = (unsigned int)ptr;number = (number & 0x7fffff) | (flag << 23);ptr = (char *)number;...
Что плохого в таком коде? Не понятно, что хотел описать
программист с точки зрения алгоритма. Да, это использование битовых
масок, но что конкретно они собирают?
В добавок к этой проблеме может возникнуть следующее: тип данных,
который задумывался программистом, может не совпасть с тем, что
получится после проведения всех операций.
Выводы
Как видно из примеров в статье, С++ это язык, в котором нужно внимательно относиться к кажущимся мелочам, начиная от форматов данных и заканчивая типами переменных. Где брать примеры? Что делать с уязвимостями? К сожалению, универсального ответа нет, но можно постоянно работать и накапливать знания о языке и его особенностях. Начать можно здесь или здесь. Так же нужно использовать плагины и приложения, которые позволяют анализировать код на этапе сборки. Можно в этом случае ориентироваться на продукты вроде этого или этого.
Узнать подробнее о курсе "C++ Developer. Professional".
Смотреть открытый вебинар на тему Области видимости и невидимости.