Привет, хабровчане. Для будущих студентов курса "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".
Смотреть открытый вебинар на тему «Области видимости и невидимости».