В моем личном списке, “что должен сделать успешный программист за свою карьеру”, есть два пункта: найти уязвимость в чужом ПО и исправить такую уязвимость. Я никогда не надеялся их реализовать т.к. предметная область мне кажется достаточно сложной. Но вчера, совершенно случайно, при регулярном использовании nanoid у меня что-то стрельнуло в голове, и я пошел копать исходники.
Через несколько минут у меня в голове уже созрел план как сломать библиотеку, а через 20 минут был код воспроизведения ошибки, он позволял получить предыдущий сгенерированный идентификатор. Фактически, это воспроизводимые коллизии, что плохо для подобного рода библиотеки, но т.к. условия эксплуатации подразумевают прямой доступ к ссылке инстанс библиотеки, проблема не выглядит серьезной.
Для нетерпеливых вот ПР с кодом воспроизведения проблемы и ее фиксом.
Детали. В вызов генератора идентификатора можно передать число для указания его длины. Чаще всего его не указывают и используется значение по умолчанию -
21. Одна из особенностей JS заключается в том что для математических операций над значениями вызывается их метод valueOf, который для Number возвращает само число, но для любых других объектов мы можем его переопределить. И возвращать разные числа по условию!Проанализировав код библиотеки, я понял, что переданный параметр длинны идентификатора передается по ссылке и в какой-то момент участвует в трех математических операциях с условиями, в которых можно создать логическую ошибку, если значение переменной будет меняться для каждого условия. В ПРе есть наглядный пример реализации.
В итоге, можно было создать такую логическую последовательность работы valueOf, при которой внутреннее состояние библиотеки не менялось бы и генерация идентификатора производилась бы на старом значении, условно говоря, сгенерированным под предыдущий идентификатор.
Интересно было и придумать исправление для этой ошибки. Не хотелось вносить большой оверхед в нано библиотеку, я сам увлекаюсь микрооптимизациями производительности и бандлсайза и для меня это был отдельный челендж. Несколько часов я экспериментировал. Первая реализация добавляла 4 байта, последующая
size += size добавляла 2 байта. В итоговой версии я добавил всего 3 символа -=0, а магия минификатора и архиватора смогла нивелировать это до ничего - бандлсайз не изменился!Библиотека разрешает передавать число строкой в аргумент, поэтому необходимо было использовать именно минус, который конвертирует строку в число, а не плюс, который для строк имеет приоритет как операция конкатенации.
В течении дня я нашел уязвимость, связался с автором, сделал фикс, отрепортил все это в snyk, который завел CVE-2021-23566. Андрей со всем помогал и сразу же зарелизил новую версию 3.1.31, за что ему отдельно спасибо.
В общем, опыт был интересный, наконец удалось получить фан от слабой динамической типизации ЖСа 🙂
#security