
Коротко о главномРазвернутьСвернуть
- В Excelize, Go-библиотеке для чтения и записи файлов Excel (около 21 тысячи звёзд на GitHub), опубликована уязвимость отказа в обслуживании.
- Файл размером 3 072 байта заставляет OpenFile крутить цикл выработки ключа столько раз, сколько указано в самом файле: при значении 100 000 000 вызов на версии 2.
- 0 занимает 58,65 секунды и только потом возвращает ошибку «zip: not a valid zip file».
В Excelize, Go-библиотеке для чтения и записи файлов Excel (около 21 тысячи звёзд на GitHub), опубликована уязвимость отказа в обслуживании. Файл размером 3 072 байта заставляет OpenFile крутить цикл выработки ключа столько раз, сколько указано в самом файле: при значении 100 000 000 вызов на версии 2.11.0 занимает 58,65 секунды и только потом возвращает ошибку «zip: not a valid zip file». Уязвимы все версии с 2.3.1 по 2.11.0, исправления на момент публикации нет, оценка CVSS 7,5.
Под угрозой сервисы, которые открывают недоверенные файлы через Excelize: импорт таблиц, конвертеры и обработчики отчётов. Ограничение размера загрузки само по себе не устраняет проблему, поскольку приведённый исследователем файл занимает всего 3 072 байта. До исправления отчёт предлагает отсеивать неподдерживаемые зашифрованные контейнеры или изолировать обработку по времени.
Как файл на три килобайта занимает библиотеку на минуту
Зашифрованные паролем книги Excel хранятся в OLE-контейнере, внутри которого лежит поток EncryptionInfo с параметрами шифрования и сам зашифрованный пакет. Excelize определяет такой файл по первым восьми байтам: функция openReaderAt ветвится только по заголовку и не смотрит, передал ли вызывающий код пароль в опциях. Дальше agileDecrypt вызывает convertPasswdToKey , где ключ выводится из пароля повторным хешированием; число повторов задаёт поле spinCount , и проверка хеша-верификатора идёт уже после этого цикла.
Само поле spinCount в коде объявлено как обычный int и заполняется голым xml.Unmarshal из потока файла. Никакой верхней границы нет, поэтому атакующий сам выбирает, сколько времени займёт вызов. Автор отчёта проверил это на выпущенных версиях с proxy.golang.org без директив replace :
v2.5.0 spinCount=10000000 5.307s
v2.9.1 spinCount=10000000 5.444s
v2.11.0 spinCount=10000000 7.529s
v2.11.0 spinCount=100000000 58.654sЦикл появился вместе с файлом crypt.go в версии 2.3.1 и с тех пор не менялся. Память во время атаки остаётся плоской, около 24 МБ, поэтому её нельзя отловить ни лимитом памяти, ни OOM-киллером: процесс просто занимает ядро на выбранное атакующим время. Автор отмечает, что уязвимость касается только доступности и не угрожает программам, которые открывают файлы, созданные их же оператором.
Чем защититься, пока нет патча
Самый простой фильтр стоит на границе: если сервис не поддерживает зашифрованные книги, отбрасывайте файлы, которые начинаются с OLE-сигнатуры D0 CF 11 E0 A1 B1 1A E1 , до вызова Excelize. Обычный XLSX это ZIP-архив и начинается с 50 4B 03 04 . Такая проверка занимает одну строку и полностью закрывает вектор для сервисов без паролей.
var oleMagic = []byte{0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1}
func looksEncrypted(head []byte) bool {
return len(head) >= 8 && bytes.Equal(head[:8], oleMagic)
}
// перед excelize.OpenReader(r, ...):
// прочитать первые 8 байт, вернуть 400, если looksEncrypted(head)Проверьте используемую версию Excelize в зависимостях проекта. В сохранённом advisory уязвимыми названы версии с 2.3.1 по 2.11.0 включительно, поле исправленных версий пусто. Следить за появлением патча следует по тому же advisory и релизам проекта; отсутствие предупреждения отдельного сканера само по себе не подтверждает безопасность зависимости.
Если зашифрованные файлы нужны, разбор стоит вынести в отдельный процесс или воркер с жёстким лимитом времени: context.Context на этом пути Excelize не поддерживается; остановить горутину снаружи невозможно. Полезно также разобрать EncryptionInfo самостоятельно и отклонить файлы, где spinCount больше, скажем, миллиона: Excel и LibreOffice обычно используют 100 000. Автор отчёта предлагает мейнтейнерам такой потолок на этапе парсинга.
Таймаут должен ограничивать именно вычисление. Возврат ошибки клиенту по истечении времени ожидания не останавливает цикл в уже запущенной горутине: в описанном пути нет механизма отмены. Изоляция в отдельном процессе позволяет завершить обработчик целиком, если он вышел за лимит. Это временное ограничение последствий; проверка недоверенного счётчика внутри библиотеки остаётся предлагаемым исправлением причины.
Почему это типовой класс ошибок для парсеров
Схема одна и та же: формат позволяет файлу самому объявить, сколько работы предстоит парсеру, а парсер верит. Вышло исправление libheif 1.23.4, где файл с неограниченным числом элементов давал квадратичное время разбора и полтора гигабайта памяти. У Excelize ситуация проще: нет ни рекурсии, ни аллокаций, только счётчик из недоверенного XML. Следить за исправлением стоит в репозитории проекта : в advisory должна появиться исправленная версия, после чего можно будет обновить зависимость и проверить импорт. О диагностике горутин в работающем сервисе рассказывает новый профиль в Go 1.27 , а о девяти уязвимостях в curl того же класса мы писали на прошлой неделе .
Источники: GHSA-jrfj-fhj2-jjvm: Unbounded spinCount in agile decryption burns CPU during OpenFile , Репозиторий qax-os/excelize
Изображение на обложке: Excelize, логотип проекта


